From aaf8509e5089ee9f60c4ccf666c1026b8f92b5b2 Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Sat, 12 Sep 2026 15:30:35 +0200 Subject: [PATCH 1/5] Copy-DbaDatabase - Stop eating the caller loop when a system database is requested Stop-Function -Continue runs PowerShell's continue. No loop encloses the system database check inside the process block, so the continue unwound out of the command and consumed an iteration of whatever loop the caller runs in: a user's foreach silently skipped an element, and Pester's runner corrupted. The escape only bites the non-EnableException path; with EnableException Stop-Function throws before it gets there. Part of #10638 (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5.1 --- public/Copy-DbaDatabase.ps1 | 5 ++++- tests/Copy-DbaDatabase.Tests.ps1 | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/public/Copy-DbaDatabase.ps1 b/public/Copy-DbaDatabase.ps1 index f353a1d5a37..fe0c9f960aa 100644 --- a/public/Copy-DbaDatabase.ps1 +++ b/public/Copy-DbaDatabase.ps1 @@ -774,7 +774,10 @@ function Copy-DbaDatabase { } if ($Database -contains "master" -or $Database -contains "msdb" -or $Database -contains "tempdb") { - Stop-Function -Message "Migrating system databases is not currently supported." -Continue + # No -Continue here: the process block has no enclosing loop, so the continue would escape + # the command and eat an iteration of whatever loop the caller runs in (#10638). + Stop-Function -Message "Migrating system databases is not currently supported." + return } try { diff --git a/tests/Copy-DbaDatabase.Tests.ps1 b/tests/Copy-DbaDatabase.Tests.ps1 index 9bf7452bd8e..27f086d7044 100644 --- a/tests/Copy-DbaDatabase.Tests.ps1 +++ b/tests/Copy-DbaDatabase.Tests.ps1 @@ -872,4 +872,27 @@ Describe $CommandName -Tag IntegrationTests { } } } + + Context "When a system database is requested" { + It "Warns without eating an iteration of the caller's loop" { + # The system database check used to run Stop-Function -Continue in the process block, where no + # loop encloses it - the continue escaped the command and consumed an iteration of this very + # loop, so the counter stayed at zero (#10638). The check fires before any connection is made. + $loopCount = 0 + foreach ($i in 1..3) { + $splatSystemDb = @{ + Source = $TestConfig.InstanceCopy1 + Destination = $TestConfig.InstanceCopy2 + Database = "master" + BackupRestore = $true + SharedPath = $TestConfig.Temp + WarningAction = "SilentlyContinue" + } + $null = Copy-DbaDatabase @splatSystemDb + $loopCount++ + } + $loopCount | Should -Be 3 + $WarnVar | Should -BeLike "*Migrating system databases is not currently supported*" + } + } } From f7e9b1b6287b21d0528dbb544c83b23dbe8ea25e Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Sun, 13 Sep 2026 11:33:39 +0200 Subject: [PATCH 2/5] Copy-DbaDatabase - Reject a system database without dropping the databases piped in after it A plain Stop-Function set the function-scope interrupt flag, so after one system database Test-FunctionInterrupt dropped every later piped database. Throw under -EnableException, otherwise warn, and return from that process invocation only. Adds one-pipeline and -EnableException tests. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5.1 --- public/Copy-DbaDatabase.ps1 | 12 ++++++-- tests/Copy-DbaDatabase.Tests.ps1 | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/public/Copy-DbaDatabase.ps1 b/public/Copy-DbaDatabase.ps1 index fe0c9f960aa..a6ee07427ea 100644 --- a/public/Copy-DbaDatabase.ps1 +++ b/public/Copy-DbaDatabase.ps1 @@ -774,9 +774,15 @@ function Copy-DbaDatabase { } if ($Database -contains "master" -or $Database -contains "msdb" -or $Database -contains "tempdb") { - # No -Continue here: the process block has no enclosing loop, so the continue would escape - # the command and eat an iteration of whatever loop the caller runs in (#10638). - Stop-Function -Message "Migrating system databases is not currently supported." + # This rejects one input, not the command: a plain Stop-Function sets the interrupt flag that + # Test-FunctionInterrupt reads at the top of this block, which would drop every database piped in + # after this one, and -Continue has no loop to continue here (#10638). So throw under + # -EnableException, otherwise warn, and return from this process invocation only. + $systemDbMessage = "Migrating system databases is not currently supported." + if ($EnableException) { + Stop-Function -Message $systemDbMessage -EnableException $true + } + Write-Message -Level Warning -Message $systemDbMessage return } diff --git a/tests/Copy-DbaDatabase.Tests.ps1 b/tests/Copy-DbaDatabase.Tests.ps1 index 27f086d7044..e5bf924874b 100644 --- a/tests/Copy-DbaDatabase.Tests.ps1 +++ b/tests/Copy-DbaDatabase.Tests.ps1 @@ -874,6 +874,55 @@ Describe $CommandName -Tag IntegrationTests { } Context "When a system database is requested" { + BeforeAll { + # We want to run all commands in the BeforeAll block with EnableException to ensure that the test fails if the setup fails. + $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + + $afterSystemDb = "dbatoolsci_aftersystem_$(Get-Random)" + $serverAfterSystem = Connect-DbaInstance -SqlInstance $TestConfig.InstanceCopy1 + $serverAfterSystem.Query("CREATE DATABASE $afterSystemDb; ALTER DATABASE $afterSystemDb SET AUTO_CLOSE OFF WITH ROLLBACK IMMEDIATE") + + # We want to run all commands outside of the BeforeAll block without EnableException to be able to test for specific warnings. + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") + } + + AfterAll { + # We want to run all commands in the AfterAll block with EnableException to ensure that the test fails if the cleanup fails. + $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + + Remove-DbaDatabase -SqlInstance $TestConfig.InstanceCopy1, $TestConfig.InstanceCopy2 -Database $afterSystemDb -ErrorAction SilentlyContinue + + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") + } + + It "Copies the database piped in after a system database" { + # A plain Stop-Function used to set the command-wide interrupt flag for the system database, and + # Test-FunctionInterrupt then dropped every database piped in after it. + $splatAfterSystem = @{ + Source = $TestConfig.InstanceCopy1 + Destination = $TestConfig.InstanceCopy2 + BackupRestore = $true + SharedPath = $NetworkPath + WarningAction = "SilentlyContinue" + } + $pipedDatabases = @(Get-DbaDatabase -SqlInstance $TestConfig.InstanceCopy1 -Database master) + @(Get-DbaDatabase -SqlInstance $TestConfig.InstanceCopy1 -Database $afterSystemDb) + $results = $pipedDatabases | Copy-DbaDatabase @splatAfterSystem + ($results | Measure-Object).Count | Should -Be 1 + $results.Name | Should -Be $afterSystemDb + $results.Status | Should -Be "Successful" + ($WarnVar -join " ") | Should -BeLike "*Migrating system databases is not currently supported*" + } + + It "Throws for a system database under -EnableException" { + $splatThrow = @{ + Destination = $TestConfig.InstanceCopy2 + BackupRestore = $true + SharedPath = $NetworkPath + EnableException = $true + } + { Get-DbaDatabase -SqlInstance $TestConfig.InstanceCopy1 -Database master | Copy-DbaDatabase @splatThrow } | Should -Throw "*Migrating system databases is not currently supported*" + } + It "Warns without eating an iteration of the caller's loop" { # The system database check used to run Stop-Function -Continue in the process block, where no # loop encloses it - the continue escaped the command and consumed an iteration of this very From b777afcef3039f4aa043044128da1dd62c252b5c Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Sun, 13 Sep 2026 11:42:28 +0200 Subject: [PATCH 3/5] Copy-DbaDatabase - Name the source in the -EnableException test (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5.1 --- tests/Copy-DbaDatabase.Tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Copy-DbaDatabase.Tests.ps1 b/tests/Copy-DbaDatabase.Tests.ps1 index e5bf924874b..6b15d96e40e 100644 --- a/tests/Copy-DbaDatabase.Tests.ps1 +++ b/tests/Copy-DbaDatabase.Tests.ps1 @@ -915,6 +915,7 @@ Describe $CommandName -Tag IntegrationTests { It "Throws for a system database under -EnableException" { $splatThrow = @{ + Source = $TestConfig.InstanceCopy1 Destination = $TestConfig.InstanceCopy2 BackupRestore = $true SharedPath = $NetworkPath From 2ecac8bb9af56e7288354c21ece298ea62d67851 Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Sun, 13 Sep 2026 11:54:47 +0200 Subject: [PATCH 4/5] Copy-DbaDatabase - Assert the -EnableException throw with the parameter form A throw inside the process block of a piped call surfaces in the upstream command, whose own output loop catches it. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5.1 --- tests/Copy-DbaDatabase.Tests.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Copy-DbaDatabase.Tests.ps1 b/tests/Copy-DbaDatabase.Tests.ps1 index 6b15d96e40e..3749132693f 100644 --- a/tests/Copy-DbaDatabase.Tests.ps1 +++ b/tests/Copy-DbaDatabase.Tests.ps1 @@ -921,7 +921,9 @@ Describe $CommandName -Tag IntegrationTests { SharedPath = $NetworkPath EnableException = $true } - { Get-DbaDatabase -SqlInstance $TestConfig.InstanceCopy1 -Database master | Copy-DbaDatabase @splatThrow } | Should -Throw "*Migrating system databases is not currently supported*" + # The parameter form: a throw inside the process block of a piped call surfaces in the upstream command, + # whose own output loop catches it (Get-DbaDatabase warns about a modified collection instead). + { Copy-DbaDatabase @splatThrow -Database master } | Should -Throw "*Migrating system databases is not currently supported*" } It "Warns without eating an iteration of the caller's loop" { From c3d4df091e23cfec604c8dd2e763df06542e123a Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Mon, 14 Sep 2026 19:36:26 +0200 Subject: [PATCH 5/5] Copy-DbaDatabase - Make the system database rejection an explicit warn-or-throw Under -EnableException the Stop-Function call throws before the line after it runs, so the warning was never written a second time, but the shape read as if it could be. Put the two paths into one if/else. The throw test now runs the command in the test scope with its own warning variable and asserts one warning record next to the exception, the one Stop-Function writes before every throw, and the loop test asserts that the last rejected call warned exactly once. (do Copy-DbaDatabase) Co-Authored-By: Claude Fable 5.1 --- public/Copy-DbaDatabase.ps1 | 3 ++- tests/Copy-DbaDatabase.Tests.ps1 | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/public/Copy-DbaDatabase.ps1 b/public/Copy-DbaDatabase.ps1 index a6ee07427ea..d06b67a5ef5 100644 --- a/public/Copy-DbaDatabase.ps1 +++ b/public/Copy-DbaDatabase.ps1 @@ -781,8 +781,9 @@ function Copy-DbaDatabase { $systemDbMessage = "Migrating system databases is not currently supported." if ($EnableException) { Stop-Function -Message $systemDbMessage -EnableException $true + } else { + Write-Message -Level Warning -Message $systemDbMessage } - Write-Message -Level Warning -Message $systemDbMessage return } diff --git a/tests/Copy-DbaDatabase.Tests.ps1 b/tests/Copy-DbaDatabase.Tests.ps1 index 3749132693f..a1e23bcb05d 100644 --- a/tests/Copy-DbaDatabase.Tests.ps1 +++ b/tests/Copy-DbaDatabase.Tests.ps1 @@ -923,7 +923,17 @@ Describe $CommandName -Tag IntegrationTests { } # The parameter form: a throw inside the process block of a piped call surfaces in the upstream command, # whose own output loop catches it (Get-DbaDatabase warns about a modified collection instead). - { Copy-DbaDatabase @splatThrow -Database master } | Should -Throw "*Migrating system databases is not currently supported*" + $systemDbException = $null + $systemDbWarnings = $null + try { + Copy-DbaDatabase @splatThrow -Database master -WarningVariable systemDbWarnings + } catch { + $systemDbException = $PSItem + } + $systemDbException.Exception.Message | Should -BeLike "*Migrating system databases is not currently supported*" + # Stop-Function writes its warning before it throws, as it does for every throw in dbatools; the message is + # not written a second time. + @($systemDbWarnings).Count | Should -Be 1 } It "Warns without eating an iteration of the caller's loop" { @@ -944,6 +954,8 @@ Describe $CommandName -Tag IntegrationTests { $loopCount++ } $loopCount | Should -Be 3 + # The last call warned once, not twice. + @($WarnVar).Count | Should -Be 1 $WarnVar | Should -BeLike "*Migrating system databases is not currently supported*" } }