From 56bf8d75e25e17794a5e906947381842fe062872 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 16:56:34 +0000 Subject: [PATCH 1/5] Restore-DbaDatabase - Add -StopAtLsn parameter for LSN-based restore Add -StopAtLsn parameter to Restore-DbaDatabase and Invoke-DbaAdvancedRestore to support restoring to a specific Log Sequence Number using SQL Server's STOPATMARK = 'lsn:' syntax. Works with -StopBefore to stop just before the specified LSN. (do Restore-DbaDatabase) Co-authored-by: Andreas Jordan --- public/Invoke-DbaAdvancedRestore.ps1 | 20 ++++++++++++++++---- public/Restore-DbaDatabase.ps1 | 19 ++++++++++++++++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/public/Invoke-DbaAdvancedRestore.ps1 b/public/Invoke-DbaAdvancedRestore.ps1 index d2a27fcb73ae..81e656edca5d 100644 --- a/public/Invoke-DbaAdvancedRestore.ps1 +++ b/public/Invoke-DbaAdvancedRestore.ps1 @@ -117,15 +117,20 @@ function Invoke-DbaAdvancedRestore { Provides more granular control than timestamp-based recovery for critical business operations. .PARAMETER StopBefore - Stops the restore operation just before the specified StopMark rather than after it. - Use this when you need to exclude a particular marked transaction from the restored database. - Only effective when used in combination with the StopMark parameter for mark-based recovery scenarios. + Stops the restore operation just before the specified StopMark or StopAtLsn rather than after it. + Use this when you need to exclude a particular marked transaction or LSN from the restored database. + Only effective when used in combination with the StopMark or StopAtLsn parameter. .PARAMETER StopAfterDate DateTime value specifying that only StopMark occurrences after this date should be considered for restore termination. Use this when the same mark name appears multiple times in your transaction log backups. Ensures the restore stops at the correct instance of the mark when identical mark names exist at different times. + .PARAMETER StopAtLsn + Log Sequence Number (LSN) in the transaction log at which to stop the restore operation. + Use this for precise point-in-time recovery to an exact LSN, which provides more granular control than timestamp-based recovery. + 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. + .PARAMETER Checksum Enables backup checksum verification during restore operations. Forces the restore to verify backup checksums and fail if checksums are not present. Use this to ensure backup files contain checksums and validate them during restore, following backup best practices. @@ -239,6 +244,7 @@ function Invoke-DbaAdvancedRestore { [switch]$StopBefore, [string]$StopMark, [datetime]$StopAfterDate, + [string]$StopAtLsn, [switch]$Checksum, [switch]$Restart, [switch]$EnableException @@ -323,7 +329,13 @@ function Invoke-DbaAdvancedRestore { } else { $restore.NoRecovery = $False } - if (-not [string]::IsNullOrEmpty($StopMark)) { + if (-not [string]::IsNullOrEmpty($StopAtLsn)) { + if ($StopBefore -eq $True) { + $restore.StopBeforeMarkName = "lsn:$StopAtLsn" + } else { + $restore.StopAtMarkName = "lsn:$StopAtLsn" + } + } elseif (-not [string]::IsNullOrEmpty($StopMark)) { if ($StopBefore -eq $True) { $restore.StopBeforeMarkName = $StopMark if ($null -ne $StopAfterDate) { diff --git a/public/Restore-DbaDatabase.ps1 b/public/Restore-DbaDatabase.ps1 index 51799e113f76..bda467fb2239 100644 --- a/public/Restore-DbaDatabase.ps1 +++ b/public/Restore-DbaDatabase.ps1 @@ -235,11 +235,16 @@ function Restore-DbaDatabase { Marked point in the transaction log to stop the restore at (Mark is created via BEGIN TRANSACTION (https://docs.microsoft.com/en-us/sql/t-sql/language-elements/begin-transaction-transact-sql?view=sql-server-ver15)). .PARAMETER StopBefore - Switch to indicate the restore should stop before StopMark occurs, default is to stop when mark is created. + Switch to indicate the restore should stop before StopMark or StopAtLsn occurs, default is to stop when mark/LSN is reached. .PARAMETER StopAfterDate By default the restore will stop at the first occurence of StopMark found in the chain, passing a datetime where will cause it to stop the first StopMark atfer that datetime. + .PARAMETER StopAtLsn + Log Sequence Number (LSN) in the transaction log at which to stop the restore operation. + Use this for precise point-in-time recovery to an exact LSN, which provides more granular control than timestamp-based recovery. + 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. + .PARAMETER Checksum Enables backup checksum verification during restore operations. Forces the restore to verify backup checksums and fail if checksums are not present. Use this to ensure backup files contain checksums and validate them during restore, following backup best practices. @@ -430,6 +435,16 @@ function Restore-DbaDatabase { Restores the backups from \\ServerName\ShareName\File as database, stops before the first 'OvernightStart' mark that occurs after '21:00 10/05/2020'. + .EXAMPLE + PS C:\> Restore-DbaDatabase -SqlInstance server1 -Path \\ServerName\ShareName\File -DatabaseName database -StopAtLsn '00000030:00000f28:0001' + + Restores the backups from \\ServerName\ShareName\File as database, stopping when the specified LSN is reached. + + .EXAMPLE + PS C:\> Restore-DbaDatabase -SqlInstance server1 -Path \\ServerName\ShareName\File -DatabaseName database -StopAtLsn '00000030:00000f28:0001' -StopBefore + + Restores the backups from \\ServerName\ShareName\File as database, stopping just before the specified LSN is reached. + Note that Date time needs to be specified in your local SQL Server culture .EXAMPLE @@ -498,6 +513,7 @@ function Restore-DbaDatabase { [switch]$StopBefore, [string]$StopMark, [datetime]$StopAfterDate = (Get-Date '01/01/1971'), + [string]$StopAtLsn, [int]$StatementTimeout = 0, [parameter(ParameterSetName = "Restore")][parameter(ParameterSetName = "RestorePage")][switch]$Checksum, [parameter(ParameterSetName = "Restore")][parameter(ParameterSetName = "RestorePage")][switch]$Restart @@ -891,6 +907,7 @@ function Restore-DbaDatabase { StopMark = $StopMark StopAfterDate = $StopAfterDate StopBefore = $StopBefore + StopAtLsn = $StopAtLsn ExecuteAs = $ExecuteAs Checksum = $Checksum Restart = $Restart From bab9bd7a731dced60277f8410a26dd924e8089f7 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:03:41 +0000 Subject: [PATCH 2/5] Add StopAtLsn to parameter validation tests (do *Restore*) Co-authored-by: Andreas Jordan --- tests/Invoke-DbaAdvancedRestore.Tests.ps1 | 1 + tests/Restore-DbaDatabase.Tests.ps1 | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Invoke-DbaAdvancedRestore.Tests.ps1 b/tests/Invoke-DbaAdvancedRestore.Tests.ps1 index e46e63c3075e..b2889eeac328 100644 --- a/tests/Invoke-DbaAdvancedRestore.Tests.ps1 +++ b/tests/Invoke-DbaAdvancedRestore.Tests.ps1 @@ -34,6 +34,7 @@ Describe $CommandName -Tag UnitTests { "StopAfterDate", "Checksum", "Restart", + "StopAtLsn", "EnableException" ) Compare-Object -ReferenceObject $expectedParameters -DifferenceObject $hasParameters | Should -BeNullOrEmpty diff --git a/tests/Restore-DbaDatabase.Tests.ps1 b/tests/Restore-DbaDatabase.Tests.ps1 index 5465876afd24..741864dd11df 100644 --- a/tests/Restore-DbaDatabase.Tests.ps1 +++ b/tests/Restore-DbaDatabase.Tests.ps1 @@ -63,7 +63,8 @@ Describe $CommandName -Tag UnitTests { "ExecuteAs", "Checksum", "Restart", - "NoXpDirRecurse" + "NoXpDirRecurse", + "StopAtLsn" ) Compare-Object -ReferenceObject $expectedParameters -DifferenceObject $hasParameters | Should -BeNullOrEmpty } From 35fc1853483f92823b11018ea7ab1cb2c55acb46 Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Sun, 29 Mar 2026 12:01:15 +0200 Subject: [PATCH 3/5] merge changes from development --- public/Restore-DbaDatabase.ps1 | 47 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/public/Restore-DbaDatabase.ps1 b/public/Restore-DbaDatabase.ps1 index bda467fb2239..ae0502d76a43 100644 --- a/public/Restore-DbaDatabase.ps1 +++ b/public/Restore-DbaDatabase.ps1 @@ -889,29 +889,30 @@ function Restore-DbaDatabase { } try { $parms = @{ - SqlInstance = $RestoreInstance - WithReplace = $WithReplace - RestoreTime = $RestoreTime - StandbyDirectory = $StandbyDirectory - NoRecovery = $NoRecovery - Continue = $Continue - OutputScriptOnly = $OutputScriptOnly - BlockSize = $BlockSize - MaxTransferSize = $MaxTransferSize - BufferCount = $Buffercount - KeepCDC = $KeepCDC - VerifyOnly = $VerifyOnly - PageRestore = $PageRestore - StorageCredential = $StorageCredential - KeepReplication = $KeepReplication - StopMark = $StopMark - StopAfterDate = $StopAfterDate - StopBefore = $StopBefore - StopAtLsn = $StopAtLsn - ExecuteAs = $ExecuteAs - Checksum = $Checksum - Restart = $Restart - EnableException = $true + SqlInstance = $RestoreInstance + WithReplace = $WithReplace + RestoreTime = $RestoreTime + StandbyDirectory = $StandbyDirectory + NoRecovery = $NoRecovery + Continue = $Continue + OutputScriptOnly = $OutputScriptOnly + BlockSize = $BlockSize + MaxTransferSize = $MaxTransferSize + BufferCount = $Buffercount + KeepCDC = $KeepCDC + ErrorBrokerConversations = $ErrorBrokerConversations + VerifyOnly = $VerifyOnly + PageRestore = $PageRestore + StorageCredential = $StorageCredential + KeepReplication = $KeepReplication + StopMark = $StopMark + StopAfterDate = $StopAfterDate + StopBefore = $StopBefore + StopAtLsn = $StopAtLsn + ExecuteAs = $ExecuteAs + Checksum = $Checksum + Restart = $Restart + EnableException = $true } $FilteredBackupHistory | Where-Object { $_.IsVerified -eq $true } | Invoke-DbaAdvancedRestore @parms } catch { From f9774fa6be74591c2a4c8ce72100d4b03342abb4 Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Sun, 29 Mar 2026 13:49:40 +0200 Subject: [PATCH 4/5] Add missing whitspace --- public/Restore-DbaDatabase.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/Restore-DbaDatabase.ps1 b/public/Restore-DbaDatabase.ps1 index 4a2b999d81a8..c8b0286da456 100644 --- a/public/Restore-DbaDatabase.ps1 +++ b/public/Restore-DbaDatabase.ps1 @@ -166,7 +166,7 @@ function Restore-DbaDatabase { Use this for log shipping secondary servers or when you need read-only access during restore operations. The directory must exist and be writable by the SQL Server service account for undo file creation. -.PARAMETER StorageCredential + .PARAMETER StorageCredential Specifies the SQL Server credential name for authenticating to Azure blob storage or S3-compatible object storage during restore operations. Use this when restoring from Azure blob storage or S3 backups that require authentication. For Azure: The credential must contain valid Azure storage account keys or SAS tokens. From 5b08bde0cb3ce4d2c58b31c786c5892932a1a822 Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Sun, 29 Mar 2026 13:49:50 +0200 Subject: [PATCH 5/5] Add test --- tests/Restore-DbaDatabase.Tests.ps1 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/Restore-DbaDatabase.Tests.ps1 b/tests/Restore-DbaDatabase.Tests.ps1 index e9a2024970f6..b984b5a0af24 100644 --- a/tests/Restore-DbaDatabase.Tests.ps1 +++ b/tests/Restore-DbaDatabase.Tests.ps1 @@ -902,7 +902,7 @@ use master } - Context -Skip "Test restoring with StopAt and StopAfterDate" { + Context -Skip "Test restoring with StopAt, StopAtLsn and StopAfterDate" { BeforeAll { $null = Get-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -ExcludeSystem -EnableException | Remove-DbaDatabase -EnableException } @@ -914,6 +914,21 @@ use master $sqlOut.ms | Should -Be 29876 $null = Remove-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Database StopAt2 } + + It "Should have stoped at lsn" { + $dbName = "TestStopAtLsn_$(Get-Random)" + $null = New-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Name $dbName + $fullBackup = Backup-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Path $backupPath + Invoke-DbaQuery -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Query "CREATE TABLE Test (id int IDENTITY)" + 1..5 | ForEach-Object -Process { Invoke-DbaQuery -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Query "INSERT INTO Test DEFAULT VALUES" } + $lsn = Invoke-DbaQuery -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Query "SELECT MAX([Current LSN]) FROM sys.fn_dblog(NULL, NULL)" -As SingleValue + 1..5 | ForEach-Object -Process { Invoke-DbaQuery -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Query "INSERT INTO Test DEFAULT VALUES" } + $logBackup = Backup-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Path $backupPath -Type Log + $null = Restore-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Path $fullBackup.Path, $logBackup.Path -DatabaseName $dbName -StopAtLsn "0x$lsn" -WithReplace + $id = Invoke-DbaQuery -SqlInstance $TestConfig.InstanceSingle -Database $dbName -Query "SELECT MAX(id) FROM Test" -As SingleValue + $id | Should -Be 5 + $null = Remove-DbaDatabase -SqlInstance $TestConfig.InstanceSingle -Database $dbName + } }