Add NewName option and recovery fixes

This commit is contained in:
2026-04-16 14:33:47 +01:00
parent 00f3f3c92e
commit 0198a20976
+117 -16
View File
@@ -10,7 +10,16 @@ param(
[string]$Action, [string]$Action,
[Parameter(Mandatory=$true)] [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 # Function to catalog backups
@@ -82,11 +91,16 @@ function Report-Catalog {
} }
Write-Host "" Write-Host ""
} }
Write-Host "Summary of Databases:"
$catalog.Keys | Sort-Object | ForEach-Object { Write-Host " - $_" }
} }
# Function to restore database # Function to restore database
function 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)) { if (-not $catalog.ContainsKey($DbName)) {
Write-Error "Database $DbName not found in catalog" 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 $latestFullKey = $dbCatalog['FULL'].Keys | Sort-Object -Descending | Select-Object -First 1
$fullFiles = $dbCatalog['FULL'][$latestFullKey] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File } $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 # Restore FULL with NORECOVERY
$fileList = $fullFiles | ForEach-Object { "DISK = '$($_.FullName)'" } $withClause = if ($moveClauses) { "WITH $($moveClauses -join ', '), NORECOVERY" } else { "WITH NORECOVERY" }
$restoreQuery = "RESTORE DATABASE [$DbName] FROM $($fileList -join ', ') 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..." Write-Host "Restoring FULL backup for $DbName..."
}
Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop 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')) { if ($dbCatalog.ContainsKey('DIFF')) {
$compatibleDiffs = @()
$diffKeys = $dbCatalog['DIFF'].Keys | Where-Object { $_ -gt $latestFullKey } | Sort-Object $diffKeys = $dbCatalog['DIFF'].Keys | Where-Object { $_ -gt $latestFullKey } | Sort-Object
foreach ($key in $diffKeys) { foreach ($key in $diffKeys) {
$diffFiles = $dbCatalog['DIFF'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File } $diffFiles = $dbCatalog['DIFF'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
$fileList = $diffFiles | ForEach-Object { "DISK = '$($_.FullName)'" } $diffHeader = Get-BackupInfo -Files $diffFiles -Instance $Instance
$restoreQuery = "RESTORE DATABASE [$DbName] FROM $($fileList -join ', ') WITH NORECOVERY" if (-not $diffHeader) {
Write-Host "Applying DIFF backup $key for $DbName..." 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 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')) { 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) { foreach ($key in $logKeys) {
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File } $logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
$fileList = $logFiles | ForEach-Object { "DISK = '$($_.FullName)'" } $fileList = $logFiles | ForEach-Object { "DISK = '$($_.FullName)'" }
$restoreQuery = "RESTORE LOG [$DbName] FROM $($fileList -join ', ') WITH NORECOVERY" $restoreQuery = "RESTORE LOG [$restoreDbName] FROM $($fileList -join ', ') WITH NORECOVERY"
Write-Host "Applying LOG backup $key for $DbName..." Write-Host "Applying LOG backup $key for $restoreDbName..."
Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop
} }
} }
# Final recovery # Final recovery
$restoreQuery = "RESTORE DATABASE [$DbName] WITH RECOVERY" $restoreQuery = "RESTORE DATABASE [$restoreDbName] WITH RECOVERY"
Write-Host "Finalizing restore for $DbName..." Write-Host "Finalizing restore for $restoreDbName..."
Invoke-Sqlcmd -ServerInstance $Instance -Query $restoreQuery -QueryTimeout 0 -ErrorAction Stop 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 # Function to print backup summary
@@ -271,6 +363,11 @@ function Get-BackupInfo {
} }
# Main script # Main script
if ($Action -ne "restore" -and $NewName) {
Write-Error "NewName is only valid with restore action"
exit 1
}
if ($Action -eq "catalog") { if ($Action -eq "catalog") {
$catalog = Catalog-Backups -Path $LiveMountRoot $catalog = Catalog-Backups -Path $LiveMountRoot
if ($DatabaseName) { if ($DatabaseName) {
@@ -287,9 +384,13 @@ if ($Action -eq "catalog") {
Write-Error "DatabaseName is required for restore action" Write-Error "DatabaseName is required for restore action"
exit 1 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 $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") { } elseif ($Action -eq "verify") {
$catalog = Catalog-Backups -Path $LiveMountRoot $catalog = Catalog-Backups -Path $LiveMountRoot
if ($DatabaseName) { if ($DatabaseName) {