Log handling and date fixes

This commit is contained in:
2026-06-10 16:56:46 +01:00
parent c09a05a129
commit 81ae0f1262
+62 -20
View File
@@ -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)
# Validate LSN continuity: the first candidate LOG must connect to the end of the FULL/DIFF chain.
# 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 = @()
}
}
$skipLogSelection = $false
if ($isPointInTimeRestore -and $candidateLogKeys.Count -eq 0) { 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" Write-Error "Point-in-time restore requested but no LOG backups are available after the selected FULL/DIFF restore point"
return return
} }
# Target is within the FULL/DIFF range; no logs needed.
$skipLogSelection = $true
}
if ($StopAtTime) { 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) {