Test LSN Chain

Quick not fully tested script to check whether a set of provided backup files contain a valid log sequence number chain.

 using namespace Microsoft.SqlServer.Management.smo
using namespace System.Collections.Generic


function Test-LSNChain{
    param(
        [Parameter(Mandatory)]
        [string[]]$File,
        [ValidateNotNullOrEmpty()]
        [string]$ComputerName = $env:computername,
        [ValidateNotNullOrEmpty()]
        $Port = 1433
    )
    begin{
        $objects = New-Object List[PSCustomObject];
    }
    process{
        
        try{
            $srv = [Server]::new("$ComputerName,$Port");
            foreach($f in $File){

                if(!(Test-Path -Path $f -PathType Leaf)){
                    throw "File $f could not be located."
                }

                $restore = [Restore]::new();
                $restore.Devices.Add(([BackupDeviceItem]::new($f, [DeviceType]::File)));
                $header = $restore.ReadBackupHeader($srv);
                $type = switch($header.BackupType){
                    (1){
                        "FULL"
                        break;
                    }
                    (5){
                        "DIFF"
                        break;
                    }
                    (2){
                        "LOG"
                        break;
                    }
                    (default){
                        throw "unsupported backup type"
                    }
                } # switch

                [void]$objects.Add([PSCustomObject]@{
                    BackupType = $type 
                    File = $f
                    CreationTime = [System.IO.File]::GetCreationTime($f)
                    ComputerName = $header.ServerName 
                    DatabaseName = $header.DatabaseName 
                    #DatabaseVersion = $header.DatabaseVersion 
                    ServerVersion = ([string]$header.SoftwareVersionMajor + '.' + [string]$header.SoftwareVersionMinor + '.' +[string]$header.SoftwareVersionBuild)
                    SizeMB = [Math]::Round($header.BackupSize/1MB,2)
                    IsCopyOnly = $header.IsCopyOnly 
                    FirstLSN = $header.FirstLSN
                    LastLSN = $header.LastLSN 
                    CheckpointLSN = $header.CheckpointLSN 
                    DatabaseBackupLSN = $header.DatabaseBackupLSN 
                    BackupStartDate = $header.BackupStartDate
                    BackupFinishDate = $header.BackupFinishDate 
                    DifferentialBaseLSN = $header.DifferentialBaseLSN 
                });
            } 
            
            # full can have a different DatabaseBackupLSN than the rest of the backups...this is just a short-circuit...the rest should all share the same backuplsn
            #if(($objects | ?{$_.BackupType -ne 'FULL'} | Group-Object -Property DatabaseBackupLSN | Measure-Object).Count -gt 1){
            if(($objects | Group-Object -Property DatabaseBackupLSN | Measure-Object).Count -gt 1){
                Write-Warning "There are more than one Database Backup LSN's listed in the backup set of files supplied."
                return $false;
            }

            $objects = ($objects | Sort BackupFinishDate);

            if($objects[0].BackupType -ne 'FULL'){
                write-warning "The first backup file $($objects[0].File) in this set is not a full backup."
                return $false;
            }

            for($i=0; $i-lt$objects.Count; $i++){
                $backup = $objects[$i];
                $nextBackup = $objects[$i+1];

                <#
                $backup | select BackupType, FirstLSN, LastLSN, CheckpointLSN, DatabaseBackupLSN, BackupStartDate, BackupFinishDate, DifferentialBaseLSN | ft -AutoSize
                $nextBackup | select BackupType, FirstLSN, LastLSN, CheckpointLSN, DatabaseBackupLSN, BackupStartDate, BackupFinishDate, DifferentialBaseLSN | ft -AutoSize
                #>

                if($null -eq $nextBackup){
                    return $true;
                }  

                if($backup.BackupType -eq 'FULL' -and $nextBackup.BackupType -eq 'DIFF'){  #diff databasebackuplsn must = full checkpointlsn
                    if($backup.CheckpointLSN -ne $nextBackup.DatabaseBackupLSN){
                        write-warning "The differential backup file $($nextBackup.File) is not the correct differential backup for the full backup $($backup.File)"
                        return $false;
                    }
                }
                elseif($backup.BackupType -eq 'FULL' -and $nextBackup.BackupType -eq 'LOG'){  #full backup must be > log.firstlsn and < log.lastlsn
                    if($backup.LastLSN -gt $nextBackup.LastLSN){
                        Write-Warning "Transaction log backup $($nextBackup.File) is too late to apply to Full backup $($backup.File)"
                        return $false;
                    }
                    if($nextBackup.LastLSN -lt $backup.FirstLSN){
                        Write-Warning "Transaction log backup $($nextBackup.File) is too recent to apply to Full backup $($backup.File)"
                        return $false;
                    }
                }
                elseif($backup.BackupType -eq 'LOG' -and $nextBackup.BackupType -eq 'LOG'){ # previous log backup lastlsn must = next log backup firstlsn
                    if($backup.LastLSN -ne $nextBackup.FirstLSN){
                        write-warning "The transaction log backup $($nextBackup.File) does not apply to the previous transaction log file $($backup.File)"
                        return $false;
                    }
                }
                elseif($backup.BackupType -eq 'DIFF' -and $nextBackup.BackupType -eq 'LOG'){  # diff checkpointlsn must match next log checkpointlsn & diff lastlsn must be > log firstlsn & < log lastlsn
                    if($backup.CheckpointLSN -ne $nextBackup.CheckpointLSN){
                        write-warning "The transaction log backup file $($nextBackup.File) does not apply to the current differential backup file $($backup.File)"
                        return $false;
                    }
                    if($backup.LastLSN -gt $nextBackup.LastLSN){
                        Write-Warning "Transaction log backup $($nextBackup.File) is too late to apply to Diff backup $($backup.File)"
                        return $false;
                    }
                    if($nextBackup.LastLSN -lt $backup.FirstLSN){
                        Write-Warning "Transaction log backup $($nextBackup.File) is too recent to apply to Diff backup $($backup.File)"
                        return $false;
                    }
                }
                elseif($backup.BackupType -eq 'LOG' -and $nextBackup.BackupType -eq 'DIFF'){  # diff lastlsn+1 must be between the log firstlsn & log lastlsn
                    if(($nextBackup.LastLSN+1) -gt $backup.FirstLSN -and ($nextBackup.LastLSN+1) -lt $backup.LastLSN){
                        write-waring "The differential backup file $($nextBackup.File) does not apply to transaction log file $($backup.File)"
                        return $false;
                    }
                }

            } # for

            

        }  # try
        catch{
            throw $_ 
        }  # catch
        finally{
            if($srv){
                $srv.ConnectionContext.Disconnect();
            }
        }  # finally

    }  # process
    end{

    } # end

} # function 

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 )

Google photo

You are commenting using your Google 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

This site uses Akismet to reduce spam. Learn how your comment data is processed.