Log handling and date fixes
This commit is contained in:
+65
-23
@@ -9,7 +9,7 @@ param(
|
|||||||
[ValidateSet("catalog", "restore", "verify")]
|
[ValidateSet("catalog", "restore", "verify")]
|
||||||
[string]$Action,
|
[string]$Action,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$SqlInstance = "sqlfcsql\TESTINST",
|
[string]$SqlInstance = "sqlfcsql\TESTINST",
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
@@ -66,7 +66,11 @@ function Catalog-Backups {
|
|||||||
$timeStr = $parts[$typeIndex + 2]
|
$timeStr = $parts[$typeIndex + 2]
|
||||||
$stripe = 0
|
$stripe = 0
|
||||||
if ($parts.Length -gt $typeIndex + 3) {
|
if ($parts.Length -gt $typeIndex + 3) {
|
||||||
$stripe = [int]$parts[$typeIndex + 3]
|
$stripeVal = 0
|
||||||
|
if (-not [int]::TryParse($parts[$typeIndex + 3], [ref]$stripeVal)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$stripe = $stripeVal
|
||||||
}
|
}
|
||||||
$key = "$dateStr$timeStr"
|
$key = "$dateStr$timeStr"
|
||||||
|
|
||||||
@@ -140,9 +144,10 @@ function Restore-Database {
|
|||||||
|
|
||||||
if ($StopAtTime) {
|
if ($StopAtTime) {
|
||||||
try {
|
try {
|
||||||
$targetStopAtTime = [datetime]::Parse($StopAtTime)
|
# Parse using the current OS locale so users can enter dates in their own format.
|
||||||
|
$targetStopAtTime = [datetime]::Parse($StopAtTime, [System.Globalization.CultureInfo]::CurrentCulture)
|
||||||
} catch {
|
} catch {
|
||||||
Write-Error "StopAtTime '$StopAtTime' is not a valid datetime value"
|
Write-Error "StopAtTime '$StopAtTime' is not a valid datetime value (expected format for your locale: $([System.Globalization.CultureInfo]::CurrentCulture.DateTimeFormat.ShortDatePattern) HH:mm:ss)"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,10 +212,7 @@ function Restore-Database {
|
|||||||
$fullFiles = $selectedFull.Files
|
$fullFiles = $selectedFull.Files
|
||||||
$fullHeader = $selectedFull.Header
|
$fullHeader = $selectedFull.Header
|
||||||
|
|
||||||
$fullBaseLsn = $fullHeader.CheckpointLSN
|
$fullBaseLsn = if ($fullHeader.CheckpointLSN) { [decimal]$fullHeader.CheckpointLSN } else { [decimal]$fullHeader.FirstLSN }
|
||||||
if (-not $fullBaseLsn) {
|
|
||||||
$fullBaseLsn = $fullHeader.FirstLSN
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create directories if specified
|
# Create directories if specified
|
||||||
if ($DataPath -and -not (Test-Path $DataPath)) {
|
if ($DataPath -and -not (Test-Path $DataPath)) {
|
||||||
@@ -277,7 +279,7 @@ function Restore-Database {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($diffHeader.DifferentialBaseLSN -ne $fullBaseLsn) {
|
if ([decimal]$diffHeader.DifferentialBaseLSN -ne $fullBaseLsn) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,15 +311,52 @@ function Restore-Database {
|
|||||||
$logStartKey = if ($selectedDiffKey) { $selectedDiffKey } else { $latestFullKey }
|
$logStartKey = if ($selectedDiffKey) { $selectedDiffKey } else { $latestFullKey }
|
||||||
$logKeys = @()
|
$logKeys = @()
|
||||||
|
|
||||||
|
# Determine the LastLSN of the last backup that will be restored (FULL or DIFF).
|
||||||
|
$lastRestoredHeader = if ($selectedDiff) {
|
||||||
|
Get-BackupInfo -Files $selectedDiff.Files -Instance $Instance
|
||||||
|
} else {
|
||||||
|
$fullHeader
|
||||||
|
}
|
||||||
|
$lastRestoredLsn = if ($lastRestoredHeader) { [decimal]$lastRestoredHeader.LastLSN } else { $null }
|
||||||
|
|
||||||
# Select LOG backups after the latest restored FULL/DIFF point.
|
# Select LOG backups after the latest restored FULL/DIFF point.
|
||||||
if ($dbCatalog.ContainsKey('LOG')) {
|
if ($dbCatalog.ContainsKey('LOG')) {
|
||||||
$candidateLogKeys = @($dbCatalog['LOG'].Keys | Where-Object { $_ -gt $logStartKey } | Sort-Object)
|
$candidateLogKeys = @($dbCatalog['LOG'].Keys | Where-Object { $_ -gt $logStartKey } | Sort-Object)
|
||||||
if ($isPointInTimeRestore -and $candidateLogKeys.Count -eq 0) {
|
|
||||||
Write-Error "Point-in-time restore requested but no LOG backups are available after the selected FULL/DIFF restore point"
|
# Validate LSN continuity: the first candidate LOG must connect to the end of the FULL/DIFF chain.
|
||||||
return
|
# If there is a gap (first LOG's FirstLSN > FULL/DIFF LastLSN), no logs can be applied.
|
||||||
|
if ($candidateLogKeys.Count -gt 0 -and $lastRestoredLsn) {
|
||||||
|
$firstLogFiles = $dbCatalog['LOG'][$candidateLogKeys[0]] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
||||||
|
$firstLogHeader = Get-BackupInfo -Files $firstLogFiles -Instance $Instance
|
||||||
|
if ($firstLogHeader -and [decimal]$firstLogHeader.FirstLSN -gt $lastRestoredLsn) {
|
||||||
|
Write-Warning "LSN gap detected: first available LOG backup ($($candidateLogKeys[0])) starts at LSN $($firstLogHeader.FirstLSN), but the restored FULL/DIFF ends at LSN $lastRestoredLsn. No log backups can be applied."
|
||||||
|
$candidateLogKeys = @()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($StopAtTime) {
|
$skipLogSelection = $false
|
||||||
|
if ($isPointInTimeRestore -and $candidateLogKeys.Count -eq 0) {
|
||||||
|
# Only error if the target point is beyond the last restored backup's finish time.
|
||||||
|
# If the target is at or before that time, no logs are required and restore can proceed.
|
||||||
|
$lastRestoredFinish = if ($lastRestoredHeader) {
|
||||||
|
try { [datetime]$lastRestoredHeader.BackupFinishDate } catch { $null }
|
||||||
|
} else { $null }
|
||||||
|
$targetIsBeyondRestored = $true
|
||||||
|
if ($targetStopAtTime -and $lastRestoredFinish -and $targetStopAtTime -le $lastRestoredFinish) {
|
||||||
|
$targetIsBeyondRestored = $false
|
||||||
|
}
|
||||||
|
if ($targetStopAtLsn -and $lastRestoredLsn -and $targetStopAtLsn -le $lastRestoredLsn) {
|
||||||
|
$targetIsBeyondRestored = $false
|
||||||
|
}
|
||||||
|
if ($targetIsBeyondRestored) {
|
||||||
|
Write-Error "Point-in-time restore requested but no LOG backups are available after the selected FULL/DIFF restore point"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
# Target is within the FULL/DIFF range; no logs needed.
|
||||||
|
$skipLogSelection = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $skipLogSelection -and $StopAtTime) {
|
||||||
$cutoffFound = $false
|
$cutoffFound = $false
|
||||||
foreach ($key in $candidateLogKeys) {
|
foreach ($key in $candidateLogKeys) {
|
||||||
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
||||||
@@ -353,7 +392,7 @@ function Restore-Database {
|
|||||||
Write-Error "StopAtTime '$StopAtTime' is not covered by available LOG backups after the selected FULL/DIFF restore point"
|
Write-Error "StopAtTime '$StopAtTime' is not covered by available LOG backups after the selected FULL/DIFF restore point"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} elseif ($StopAtLSN) {
|
} elseif (-not $skipLogSelection -and $StopAtLSN) {
|
||||||
$cutoffFound = $false
|
$cutoffFound = $false
|
||||||
foreach ($key in $candidateLogKeys) {
|
foreach ($key in $candidateLogKeys) {
|
||||||
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
||||||
@@ -395,7 +434,7 @@ function Restore-Database {
|
|||||||
$logKeys = $candidateLogKeys
|
$logKeys = $candidateLogKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isPointInTimeRestore -and $logKeys.Count -eq 0) {
|
if (-not $skipLogSelection -and $isPointInTimeRestore -and $logKeys.Count -eq 0) {
|
||||||
Write-Error "Point-in-time restore requested but no usable LOG backups were selected"
|
Write-Error "Point-in-time restore requested but no usable LOG backups were selected"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -431,7 +470,7 @@ function Restore-Database {
|
|||||||
}
|
}
|
||||||
if ($isPointInTimeRestore) {
|
if ($isPointInTimeRestore) {
|
||||||
if ($StopAtTime) {
|
if ($StopAtTime) {
|
||||||
Write-Host "PITR Target: STOPAT '$StopAtTime'"
|
Write-Host "PITR Target: STOPAT '$($targetStopAtTime.ToString([System.Globalization.CultureInfo]::CurrentCulture))'"
|
||||||
} else {
|
} else {
|
||||||
$previewLsn = if ($StopAtLSN -like 'lsn:*') { $StopAtLSN } else { "lsn:$StopAtLSN" }
|
$previewLsn = if ($StopAtLSN -like 'lsn:*') { $StopAtLSN } else { "lsn:$StopAtLSN" }
|
||||||
Write-Host "PITR Target: STOPATMARK '$previewLsn'"
|
Write-Host "PITR Target: STOPATMARK '$previewLsn'"
|
||||||
@@ -473,8 +512,11 @@ function Restore-Database {
|
|||||||
# Apply STOPAT/STOPATMARK only on the final LOG restore statement.
|
# Apply STOPAT/STOPATMARK only on the final LOG restore statement.
|
||||||
if ($isPointInTimeRestore -and $i -eq ($logKeys.Count - 1)) {
|
if ($isPointInTimeRestore -and $i -eq ($logKeys.Count - 1)) {
|
||||||
if ($StopAtTime) {
|
if ($StopAtTime) {
|
||||||
$escapedStopAtTime = $StopAtTime.Replace("'", "''")
|
# Use the parsed datetime formatted as ISO 8601, not the raw input string.
|
||||||
$logWithClause = "$logWithClause, STOPAT = '$escapedStopAtTime'"
|
# SQL Server requires a consistent datetime format; the user's input may be
|
||||||
|
# locale-specific (e.g. dd/MM/yyyy) which SQL Server does not accept.
|
||||||
|
$sqlStopAt = $targetStopAtTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
|
$logWithClause = "$logWithClause, STOPAT = '$sqlStopAt'"
|
||||||
} elseif ($StopAtLSN) {
|
} elseif ($StopAtLSN) {
|
||||||
$normalizedLsn = if ($StopAtLSN -like "lsn:*") { $StopAtLSN } else { "lsn:$StopAtLSN" }
|
$normalizedLsn = if ($StopAtLSN -like "lsn:*") { $StopAtLSN } else { "lsn:$StopAtLSN" }
|
||||||
$escapedLsn = $normalizedLsn.Replace("'", "''")
|
$escapedLsn = $normalizedLsn.Replace("'", "''")
|
||||||
@@ -510,7 +552,7 @@ function Print-BackupSummary {
|
|||||||
foreach ($item in $fullItems) {
|
foreach ($item in $fullItems) {
|
||||||
$header = @($item.Header)[0]
|
$header = @($item.Header)[0]
|
||||||
if (-not $header) { continue }
|
if (-not $header) { continue }
|
||||||
Write-Host " Date: $($header.BackupFinishDate) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
|
Write-Host " Date: $(([datetime]$header.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +563,7 @@ function Print-BackupSummary {
|
|||||||
foreach ($item in $diffItems) {
|
foreach ($item in $diffItems) {
|
||||||
$header = @($item.Header)[0]
|
$header = @($item.Header)[0]
|
||||||
if (-not $header) { continue }
|
if (-not $header) { continue }
|
||||||
Write-Host " Date: $($header.BackupFinishDate) | Base LSN: $($header.DifferentialBaseLSN) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
|
Write-Host " Date: $(([datetime]$header.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) | Base LSN: $($header.DifferentialBaseLSN) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +574,7 @@ function Print-BackupSummary {
|
|||||||
$firstLog = @($logItems[0].Header)[0]
|
$firstLog = @($logItems[0].Header)[0]
|
||||||
$lastLog = @($logItems[-1].Header)[0]
|
$lastLog = @($logItems[-1].Header)[0]
|
||||||
Write-Host "LOG Backups:"
|
Write-Host "LOG Backups:"
|
||||||
Write-Host " Point-in-Time Range: $($firstLog.BackupStartDate) to $($lastLog.BackupFinishDate)"
|
Write-Host " Point-in-Time Range: $(([datetime]$firstLog.BackupStartDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) to $(([datetime]$lastLog.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))"
|
||||||
Write-Host " LSN Range: $($firstLog.FirstLSN) - $($lastLog.LastLSN)"
|
Write-Host " LSN Range: $($firstLog.FirstLSN) - $($lastLog.LastLSN)"
|
||||||
|
|
||||||
# Check for gaps
|
# Check for gaps
|
||||||
@@ -590,8 +632,8 @@ function Verify-Backups {
|
|||||||
$header = Get-BackupInfo -Files $files -Instance $Instance
|
$header = Get-BackupInfo -Files $files -Instance $Instance
|
||||||
if ($header) {
|
if ($header) {
|
||||||
Write-Host " Backup Details:"
|
Write-Host " Backup Details:"
|
||||||
Write-Host " Start Date: $($header.BackupStartDate)"
|
Write-Host " Start Date: $(([datetime]$header.BackupStartDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))"
|
||||||
Write-Host " Finish Date: $($header.BackupFinishDate)"
|
Write-Host " Finish Date: $(([datetime]$header.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))"
|
||||||
Write-Host " First LSN: $($header.FirstLSN)"
|
Write-Host " First LSN: $($header.FirstLSN)"
|
||||||
Write-Host " Last LSN: $($header.LastLSN)"
|
Write-Host " Last LSN: $($header.LastLSN)"
|
||||||
if ($header.DatabaseBackupLSN) {
|
if ($header.DatabaseBackupLSN) {
|
||||||
|
|||||||
Reference in New Issue
Block a user