Skip to content

Commit 6dd5947

Browse files
Restore-DbaDatabase - Fix StopAtLsn format handling (review of #10245)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0e95414 commit 6dd5947

5 files changed

Lines changed: 64 additions & 7 deletions

File tree

docs/trackers/features/commit-bug-review-TRACKER.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Find real bugs (logic errors, null refs, incorrect behavior) and fix them. Skip
104104
| 7b349b546 | Test-DbaPath - Handle xp_fileexist execution failures gracefully (#10288) | DONE | Restored -EnableException behavior for xp_fileexist failures and added unit regression tests. |
105105
| 89c06e287 | Invoke-DbaCycleErrorLog - Fix example command names (#10290) | DONE | Reviewed help-text-only example-name fix; no bugs found. |
106106
| 899cdc30c | Update-SqlPermission - Remove unnecessary SqlConnectionObject.Close() calls (#10291) | DONE | Materialized SMO enumerations to avoid open DataReader conflicts and added a unit regression test. |
107-
| 97d03bee3 | Restore-DbaDatabase - Add -StopAtLsn parameter for LSN-based restore (#10245) | PENDING | |
107+
| 97d03bee3 | Restore-DbaDatabase - Add -StopAtLsn parameter for LSN-based restore (#10245) | DONE | Normalized StopAtLsn input so sys.fn_dblog and lsn:-prefixed values restore correctly; added unit regression tests. |
108108
| 1fdfddee6 | New-DbaFirewallRule - Fix binary path extraction and remove dead code (#10294) | PENDING | |
109109
| aaa8f9eaa | Add ReleaseDate for SQL Server releases to buildref-index / Get-DbaBuild / Test-DbaBuild / Add -MaxTimeBehind (#10277) | PENDING | |
110110
| c16f7f349 | Export-DbaCredential - Add IF NOT EXISTS guard to exported SQL scripts (#10295) | PENDING | |

public/Invoke-DbaAdvancedRestore.ps1

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ function Invoke-DbaAdvancedRestore {
134134
.PARAMETER StopAtLsn
135135
Log Sequence Number (LSN) in the transaction log at which to stop the restore operation.
136136
Use this for precise point-in-time recovery to an exact LSN, which provides more granular control than timestamp-based recovery.
137-
The LSN value can be obtained from sys.fn_dblog, backup headers, or error logs. Combine with -StopBefore to stop just before the specified LSN.
137+
Accepts either the numeric restore format used by SQL Server or the colon-delimited format returned by sys.fn_dblog.
138+
Combine with -StopBefore to stop just before the specified LSN.
138139
139140
.PARAMETER Checksum
140141
Enables backup checksum verification during restore operations. Forces the restore to verify backup checksums and fail if checksums are not present.
@@ -270,6 +271,34 @@ function Invoke-DbaAdvancedRestore {
270271
Stop-Function -Category InvalidArgument -Message "ErrorBrokerConversations cannot be specified with Norecovery or Standby as it needs recovery to work"
271272
return
272273
}
274+
if (-not [string]::IsNullOrWhiteSpace($StopAtLsn)) {
275+
$stopAtLsnValue = $StopAtLsn.Trim()
276+
if ($stopAtLsnValue -like "lsn:*") {
277+
$stopAtLsnValue = $stopAtLsnValue.Substring(4)
278+
}
279+
if ($stopAtLsnValue -like "0x*") {
280+
$stopAtLsnValue = $stopAtLsnValue.Substring(2)
281+
}
282+
if ($stopAtLsnValue -notmatch "^[0-9]+$") {
283+
$splatLsnConversion = @{
284+
LSN = $stopAtLsnValue
285+
EnableException = $true
286+
}
287+
$message = "StopAtLsn must be a numeric restore LSN or a colon-delimited value such as 00000030:00000f28:0001."
288+
try {
289+
$convertedLsn = Convert-DbaLSN @splatLsnConversion
290+
} catch {
291+
Stop-Function -Category InvalidArgument -Message $message -ErrorRecord $_
292+
return
293+
}
294+
if ($null -eq $convertedLsn -or [string]::IsNullOrWhiteSpace($convertedLsn.Numeric)) {
295+
Stop-Function -Category InvalidArgument -Message $message
296+
return
297+
}
298+
$stopAtLsnValue = $convertedLsn.Numeric
299+
}
300+
$StopAtLsn = $stopAtLsnValue
301+
}
273302

274303
if ($null -ne $PageRestore) {
275304
Write-Message -Message "Doing Page Recovery" -Level Verbose

public/Restore-DbaDatabase.ps1

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,8 @@ function Restore-DbaDatabase {
248248
.PARAMETER StopAtLsn
249249
Log Sequence Number (LSN) in the transaction log at which to stop the restore operation.
250250
Use this for precise point-in-time recovery to an exact LSN, which provides more granular control than timestamp-based recovery.
251-
The LSN value can be obtained from sys.fn_dblog, backup headers, or error logs. Combine with -StopBefore to stop just before the specified LSN.
251+
Accepts either the numeric restore format used by SQL Server or the colon-delimited format returned by sys.fn_dblog.
252+
Combine with -StopBefore to stop just before the specified LSN.
252253
253254
.PARAMETER Checksum
254255
Enables backup checksum verification during restore operations. Forces the restore to verify backup checksums and fail if checksums are not present.
@@ -441,12 +442,12 @@ function Restore-DbaDatabase {
441442
Restores the backups from \\ServerName\ShareName\File as database, stops before the first 'OvernightStart' mark that occurs after '21:00 10/05/2020'.
442443
443444
.EXAMPLE
444-
PS C:\> Restore-DbaDatabase -SqlInstance server1 -Path \\ServerName\ShareName\File -DatabaseName database -StopAtLsn '00000030:00000f28:0001'
445+
PS C:\> Restore-DbaDatabase -SqlInstance server1 -Path \\ServerName\ShareName\File -DatabaseName database -StopAtLsn "00000030:00000f28:0001"
445446
446447
Restores the backups from \\ServerName\ShareName\File as database, stopping when the specified LSN is reached.
447448
448449
.EXAMPLE
449-
PS C:\> Restore-DbaDatabase -SqlInstance server1 -Path \\ServerName\ShareName\File -DatabaseName database -StopAtLsn '00000030:00000f28:0001' -StopBefore
450+
PS C:\> Restore-DbaDatabase -SqlInstance server1 -Path \\ServerName\ShareName\File -DatabaseName database -StopAtLsn "00000030:00000f28:0001" -StopBefore
450451
451452
Restores the backups from \\ServerName\ShareName\File as database, stopping just before the specified LSN is reached.
452453

tests/Invoke-DbaAdvancedRestore.Tests.ps1

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ Describe $CommandName -Tag UnitTests {
112112
BeforeEach {
113113
Mock Connect-DbaInstance { $script:mockServer }
114114
Mock New-Object {
115-
New-MockRestore
115+
$script:lastRestore = New-MockRestore
116+
$script:lastRestore
116117
} -ParameterFilter {
117118
$TypeName -eq "Microsoft.SqlServer.Management.Smo.Restore"
118119
}
@@ -150,6 +151,32 @@ Describe $CommandName -Tag UnitTests {
150151
$scriptOutput | Should -BeLike "EXECUTE AS LOGIN='RestoreAs'*ERROR_BROKER_CONVERSATIONS*"
151152
Should -Invoke Stop-Function -Times 0
152153
}
154+
155+
It "Should convert fn_dblog-style StopAtLsn values before scripting the restore" {
156+
Mock Stop-Function { }
157+
$null = Invoke-DbaAdvancedRestore -BackupHistory $script:backupHistory -SqlInstance "sql1" -OutputScriptOnly -StopAtLsn "00000014:000000f3:0001"
158+
159+
$script:lastRestore.StopAtMarkName | Should -Be "lsn:20000000024300001"
160+
$script:lastRestore.StopBeforeMarkName | Should -BeNullOrEmpty
161+
Should -Invoke Stop-Function -Times 0
162+
}
163+
164+
It "Should respect StopBefore when StopAtLsn already includes the SQL lsn prefix" {
165+
Mock Stop-Function { }
166+
$null = Invoke-DbaAdvancedRestore -BackupHistory $script:backupHistory -SqlInstance "sql1" -OutputScriptOnly -StopAtLsn "lsn:20000000024300001" -StopBefore
167+
168+
$script:lastRestore.StopBeforeMarkName | Should -Be "lsn:20000000024300001"
169+
$script:lastRestore.StopAtMarkName | Should -BeNullOrEmpty
170+
Should -Invoke Stop-Function -Times 0
171+
}
172+
173+
It "Should reject invalid StopAtLsn values" {
174+
Mock Stop-Function {
175+
throw $Message
176+
}
177+
178+
{ Invoke-DbaAdvancedRestore -BackupHistory $script:backupHistory -SqlInstance "sql1" -OutputScriptOnly -StopAtLsn "bad-lsn" } | Should -Throw "*StopAtLsn must be a numeric restore LSN or a colon-delimited value*"
179+
}
153180
}
154181
}
155182
}

tests/Restore-DbaDatabase.Tests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,7 @@ use master
987987
$lsn = Invoke-DbaQuery -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Query "SELECT MAX([Current LSN]) FROM sys.fn_dblog(NULL, NULL)" -As SingleValue
988988
1..5 | ForEach-Object -Process { Invoke-DbaQuery -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Query "INSERT INTO Test DEFAULT VALUES" }
989989
$logBackup = Backup-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Path $backupPath -Type Log
990-
$null = Restore-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Path $fullBackup.Path, $logBackup.Path -DatabaseName $dbName -StopAtLsn "0x$lsn" -WithReplace
990+
$null = Restore-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Path $fullBackup.Path, $logBackup.Path -DatabaseName $dbName -StopAtLsn $lsn -WithReplace
991991
$id = Invoke-DbaQuery -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Query "SELECT MAX(id) FROM Test" -As SingleValue
992992
$id | Should -Be 5
993993
$null = Remove-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Database $dbName

0 commit comments

Comments
 (0)