Add NewName option and recovery fixes
This commit is contained in:
+118
-17
@@ -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"
|
||||||
Write-Host "Restoring FULL backup for $DbName..."
|
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
|
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user