From 0198a2097602bfc1f4b1732bbaf0b6bb5de8bb7e Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Thu, 16 Apr 2026 14:33:47 +0100 Subject: [PATCH] Add NewName option and recovery fixes --- RestoreScript.ps1 | 135 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 17 deletions(-) diff --git a/RestoreScript.ps1 b/RestoreScript.ps1 index 2936967..7a6a74d 100644 --- a/RestoreScript.ps1 +++ b/RestoreScript.ps1 @@ -10,7 +10,16 @@ param( [string]$Action, [Parameter(Mandatory=$true)] - [string]$SqlInstance = "sqlfcsql\TESTINST" + [string]$SqlInstance = "sqlfcsql\TESTINST", + + [Parameter(Mandatory=$false)] + [string]$DataPath, + + [Parameter(Mandatory=$false)] + [string]$LogPath, + + [Parameter(Mandatory=$false)] + [string]$NewName ) # Function to catalog backups @@ -82,11 +91,16 @@ function Report-Catalog { } Write-Host "" } + + Write-Host "Summary of Databases:" + $catalog.Keys | Sort-Object | ForEach-Object { Write-Host " - $_" } } # Function to restore database function Restore-Database { - param([string]$DbName, [hashtable]$Catalog, [string]$Instance) + param([string]$DbName, [hashtable]$Catalog, [string]$Instance, [string]$DataPath, [string]$LogPath, [string]$TargetDbName) + + $restoreDbName = if ($TargetDbName) { $TargetDbName } else { $DbName } if (-not $catalog.ContainsKey($DbName)) { Write-Error "Database $DbName not found in catalog" @@ -103,43 +117,121 @@ function Restore-Database { $latestFullKey = $dbCatalog['FULL'].Keys | Sort-Object -Descending | Select-Object -First 1 $fullFiles = $dbCatalog['FULL'][$latestFullKey] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File } + $fullHeader = Get-BackupInfo -Files $fullFiles -Instance $Instance + if (-not $fullHeader) { + Write-Error "Failed to read FULL backup header for database $DbName" + return + } + + $fullBaseLsn = $fullHeader.CheckpointLSN + if (-not $fullBaseLsn) { + $fullBaseLsn = $fullHeader.FirstLSN + } + + # Create directories if specified + if ($DataPath -and -not (Test-Path $DataPath)) { + New-Item -ItemType Directory -Path $DataPath -Force + } + if ($LogPath -and -not (Test-Path $LogPath)) { + New-Item -ItemType Directory -Path $LogPath -Force + } + + # Get file list for MOVE clauses + $fileListStr = $fullFiles | ForEach-Object { "DISK = '$($_.FullName)'" } + $fileListOnlyQuery = "RESTORE FILELISTONLY FROM $($fileListStr -join ', ')" + $fileListResult = Invoke-Sqlcmd -ServerInstance $Instance -Query $fileListOnlyQuery -QueryTimeout 0 -ErrorAction Stop + + $moveClauses = @() + $dataFileIndex = 0 + $logFileIndex = 0 + foreach ($file in $fileListResult) { + $logicalName = $file.LogicalName + $type = $file.Type + $originalName = $file.PhysicalName + $extension = [System.IO.Path]::GetExtension($originalName) + $baseName = [System.IO.Path]::GetFileName($originalName) + + # When restoring as a different database name, rewrite physical file names + # so they do not clash with the source database files on disk. + if ($TargetDbName) { + if ($type -eq 'D') { + $dataFileIndex++ + $newBaseName = if ($dataFileIndex -eq 1) { "${restoreDbName}_Data" } else { "${restoreDbName}_Data$dataFileIndex" } + $baseName = "$newBaseName$extension" + } elseif ($type -eq 'L') { + $logFileIndex++ + $newBaseName = if ($logFileIndex -eq 1) { "${restoreDbName}_Log" } else { "${restoreDbName}_Log$logFileIndex" } + $baseName = "$newBaseName$extension" + } + } + + if ($type -eq 'D' -and $DataPath) { + $newPath = Join-Path $DataPath $baseName + $moveClauses += "MOVE '$logicalName' TO '$newPath'" + } elseif ($type -eq 'L' -and $LogPath) { + $newPath = Join-Path $LogPath $baseName + $moveClauses += "MOVE '$logicalName' TO '$newPath'" + } + } # Restore FULL with NORECOVERY - $fileList = $fullFiles | ForEach-Object { "DISK = '$($_.FullName)'" } - $restoreQuery = "RESTORE DATABASE [$DbName] FROM $($fileList -join ', ') WITH NORECOVERY" - Write-Host "Restoring FULL backup for $DbName..." + $withClause = if ($moveClauses) { "WITH $($moveClauses -join ', '), NORECOVERY" } else { "WITH NORECOVERY" } + $restoreQuery = "RESTORE DATABASE [$restoreDbName] FROM $($fileListStr -join ', ') $withClause" + if ($TargetDbName) { + Write-Host "Restoring FULL backup for $DbName as $restoreDbName..." + } else { + Write-Host "Restoring FULL backup for $DbName..." + } Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop - # Apply DIFF backups after the FULL + # Choose the latest DIFF that is compatible with the selected FULL. + $selectedDiffKey = $null if ($dbCatalog.ContainsKey('DIFF')) { + $compatibleDiffs = @() $diffKeys = $dbCatalog['DIFF'].Keys | Where-Object { $_ -gt $latestFullKey } | Sort-Object foreach ($key in $diffKeys) { $diffFiles = $dbCatalog['DIFF'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File } - $fileList = $diffFiles | ForEach-Object { "DISK = '$($_.FullName)'" } - $restoreQuery = "RESTORE DATABASE [$DbName] FROM $($fileList -join ', ') WITH NORECOVERY" - Write-Host "Applying DIFF backup $key for $DbName..." + $diffHeader = Get-BackupInfo -Files $diffFiles -Instance $Instance + if (-not $diffHeader) { + Write-Warning "Skipping DIFF backup $key for $DbName because its header could not be read" + continue + } + + if ($diffHeader.DifferentialBaseLSN -eq $fullBaseLsn) { + $compatibleDiffs += @{ Key = $key; Files = $diffFiles } + } + } + + if ($compatibleDiffs.Count -gt 0) { + $selectedDiff = $compatibleDiffs | Sort-Object { $_.Key } -Descending | Select-Object -First 1 + $selectedDiffKey = $selectedDiff.Key + $fileList = $selectedDiff.Files | ForEach-Object { "DISK = '$($_.FullName)'" } + $restoreQuery = "RESTORE DATABASE [$restoreDbName] FROM $($fileList -join ', ') WITH NORECOVERY" + Write-Host "Applying DIFF backup $selectedDiffKey for $restoreDbName..." Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop } } - # Apply LOG backups after the FULL + $logStartKey = if ($selectedDiffKey) { $selectedDiffKey } else { $latestFullKey } + + # Apply LOG backups after the latest restored FULL/DIFF point if ($dbCatalog.ContainsKey('LOG')) { - $logKeys = $dbCatalog['LOG'].Keys | Where-Object { $_ -gt $latestFullKey } | Sort-Object + $logKeys = $dbCatalog['LOG'].Keys | Where-Object { $_ -gt $logStartKey } | Sort-Object foreach ($key in $logKeys) { $logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File } $fileList = $logFiles | ForEach-Object { "DISK = '$($_.FullName)'" } - $restoreQuery = "RESTORE LOG [$DbName] FROM $($fileList -join ', ') WITH NORECOVERY" - Write-Host "Applying LOG backup $key for $DbName..." + $restoreQuery = "RESTORE LOG [$restoreDbName] FROM $($fileList -join ', ') WITH NORECOVERY" + Write-Host "Applying LOG backup $key for $restoreDbName..." Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop } } # Final recovery - $restoreQuery = "RESTORE DATABASE [$DbName] WITH RECOVERY" - Write-Host "Finalizing restore for $DbName..." + $restoreQuery = "RESTORE DATABASE [$restoreDbName] WITH RECOVERY" + Write-Host "Finalizing restore for $restoreDbName..." Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop - Write-Host "Restore completed for $DbName" + Write-Host "Restore completed for $restoreDbName" } # Function to print backup summary @@ -271,6 +363,11 @@ function Get-BackupInfo { } # Main script +if ($Action -ne "restore" -and $NewName) { + Write-Error "NewName is only valid with restore action" + exit 1 +} + if ($Action -eq "catalog") { $catalog = Catalog-Backups -Path $LiveMountRoot if ($DatabaseName) { @@ -287,9 +384,13 @@ if ($Action -eq "catalog") { Write-Error "DatabaseName is required for restore action" exit 1 } + if (-not $DataPath -or -not $LogPath) { + Write-Error "DataPath and LogPath are required for restore action" + exit 1 + } $catalog = Catalog-Backups -Path $LiveMountRoot - Restore-Database -DbName $DatabaseName -Catalog $catalog -Instance $SqlInstance + Restore-Database -DbName $DatabaseName -Catalog $catalog -Instance $SqlInstance -DataPath $DataPath -LogPath $LogPath -TargetDbName $NewName } elseif ($Action -eq "verify") { $catalog = Catalog-Backups -Path $LiveMountRoot if ($DatabaseName) {