From 950ddf06e360e69e02da9f251d664afd44889ad6 Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Fri, 14 Aug 2026 15:48:26 +0200 Subject: [PATCH] Database context - Run server level statements on the connection Ten call sites went through a database object only because they needed somewhere to run a statement. The execution manager of an SMO database is the connection context of the parent server, so each of them issued a USE and left the connection of the caller in master or msdb. None of the statements needed a database context in the first place. They now run on the connection itself: - Export-DbaLogin, New-DbaLogin, Get-LoginPasswordHash read a password hash from sys.sql_logins or sys.server_principals. In all three the primary path already used ConnectionContext.ExecuteScalar and only the fallback went through master. - Get-DbaDbDetachedFileInfo resolves a collation with fn_helpcollations, which is available in every database. - Get-OfflineSqlFileStructure reads SERVERPROPERTY. - Set-DbaTempDbConfig executes ALTER DATABASE tempdb statements. - Remove-DbaAgentJob called sp_delete_job in msdb. The procedure is now named in full as msdb.dbo.sp_delete_job, so the connection does not have to go there. The help of Connect-DbaInstance recommended the pattern this removes, so it now points at the connection context and says why. This is the part of #10555 that needs no new mechanism, so it is separate from the script method fix in #10579. Set-DbaTempDbConfig also reads tempdb through $server.Databases['tempdb'].Query(), which is that other fix; the command is only free of the leak once both are in. (do Export-DbaLogin, New-DbaLogin, Get-DbaDbDetachedFileInfo, Set-DbaTempDbConfig, Remove-DbaAgentJob, Sync-DbaLoginPassword, Connect-DbaInstance) Co-Authored-By: Claude Opus 5 (1M context) --- private/functions/Get-LoginPasswordHash.ps1 | 4 ++- .../functions/Get-OfflineSqlFileStructure.ps1 | 4 ++- public/Connect-DbaInstance.ps1 | 4 ++- public/Export-DbaLogin.ps1 | 4 ++- public/Get-DbaDbDetachedFileInfo.ps1 | 4 ++- public/New-DbaLogin.ps1 | 4 ++- public/Remove-DbaAgentJob.ps1 | 6 ++-- public/Set-DbaTempDbConfig.ps1 | 5 ++- tests/Get-DbaDbDetachedFileInfo.Tests.ps1 | 34 +++++++++++++++++++ tests/Remove-DbaAgentJob.Tests.ps1 | 34 +++++++++++++++++++ 10 files changed, 94 insertions(+), 9 deletions(-) diff --git a/private/functions/Get-LoginPasswordHash.ps1 b/private/functions/Get-LoginPasswordHash.ps1 index 11dc3b7b2b19..343e1f7e871a 100644 --- a/private/functions/Get-LoginPasswordHash.ps1 +++ b/private/functions/Get-LoginPasswordHash.ps1 @@ -63,7 +63,9 @@ function Get-LoginPasswordHash { $hashedPass = $server.ConnectionContext.ExecuteScalar($sql) } catch { try { - $hashedPassDt = $server.Databases["master"].ExecuteWithResults($sql) + # Same query as above, so it runs on the connection as well. Going through the master + # database would leave the connection of the caller there. See #10555. + $hashedPassDt = $server.ConnectionContext.ExecuteWithResults($sql) $hashedPass = $hashedPassDt.Tables[0].Rows[0].Item(0) } catch { Stop-Function -Message "Failed to retrieve password hash for login $($Login.Name)" -ErrorRecord $_ -Target $Login -Continue diff --git a/private/functions/Get-OfflineSqlFileStructure.ps1 b/private/functions/Get-OfflineSqlFileStructure.ps1 index a29dd02d08ac..9bb094f55375 100644 --- a/private/functions/Get-OfflineSqlFileStructure.ps1 +++ b/private/functions/Get-OfflineSqlFileStructure.ps1 @@ -27,7 +27,9 @@ Internal function. Returns dictionary object that contains file structures for S if ($filestream) { $sql = "SELECT COALESCE(SERVERPROPERTY('FilestreamConfiguredLevel'),0) AS fs" - $fscheck = $server.databases['master'].ExecuteWithResults($sql) + # SERVERPROPERTY does not depend on the current database, so this runs on the connection. Going + # through the master database would leave the connection of the caller there. See #10555. + $fscheck = $server.ConnectionContext.ExecuteWithResults($sql) if ($fscheck.tables.fs -eq 0) { return $false } } diff --git a/public/Connect-DbaInstance.ps1 b/public/Connect-DbaInstance.ps1 index eba1d4953136..73aaf17997e5 100644 --- a/public/Connect-DbaInstance.ps1 +++ b/public/Connect-DbaInstance.ps1 @@ -19,7 +19,9 @@ function Connect-DbaInstance { https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnectionstringbuilder.aspx https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.aspx - To execute SQL commands directly: $server.ConnectionContext.ExecuteReader($sql) or $server.Databases['master'].ExecuteNonQuery($sql) + To execute SQL commands directly: $server.ConnectionContext.ExecuteReader($sql) or $server.ConnectionContext.ExecuteNonQuery($sql) + + Run statements through the connection context rather than through a database object. A database object executes on the same connection and leaves it in that database, which changes the database of every later command that reuses the connection. .PARAMETER SqlInstance The target SQL Server instance or instances. This can be a collection and receive pipeline input to allow the function to be executed against multiple SQL Server instances. diff --git a/public/Export-DbaLogin.ps1 b/public/Export-DbaLogin.ps1 index a285ce4af87a..96ba8850c4df 100644 --- a/public/Export-DbaLogin.ps1 +++ b/public/Export-DbaLogin.ps1 @@ -390,7 +390,9 @@ function Export-DbaLogin { try { $hashedPass = $server.ConnectionContext.ExecuteScalar($sql) } catch { - $hashedPassDt = $server.Databases['master'].ExecuteWithResults($sql) + # Same query as above, so it runs on the connection as well. Going through the + # master database would leave the connection of the caller there. See #10555. + $hashedPassDt = $server.ConnectionContext.ExecuteWithResults($sql) $hashedPass = $hashedPassDt.Tables[0].Rows[0].Item(0) } diff --git a/public/Get-DbaDbDetachedFileInfo.ps1 b/public/Get-DbaDbDetachedFileInfo.ps1 index c1f76ae16761..655e4cc995ba 100644 --- a/public/Get-DbaDbDetachedFileInfo.ps1 +++ b/public/Get-DbaDbDetachedFileInfo.ps1 @@ -130,7 +130,9 @@ function Get-DbaDbDetachedFileInfo { $collationsql = "SELECT name FROM fn_helpcollations() WHERE COLLATIONPROPERTY(name, N'COLLATIONID') = $collationid" try { - $dataset = $server.databases['master'].ExecuteWithResults($collationsql) + # fn_helpcollations is available in every database, so this runs on the connection. Going + # through the master database would leave the connection of the caller there. See #10555. + $dataset = $server.ConnectionContext.ExecuteWithResults($collationsql) $collation = "$($dataset.Tables[0].Rows[0].Item(0))" } catch { $collation = $collationid diff --git a/public/New-DbaLogin.ps1 b/public/New-DbaLogin.ps1 index d542c57deef6..90273f46378a 100644 --- a/public/New-DbaLogin.ps1 +++ b/public/New-DbaLogin.ps1 @@ -327,7 +327,9 @@ function New-DbaLogin { try { $hashedPass = $sourceServer.ConnectionContext.ExecuteScalar($sql) } catch { - $hashedPassDt = $sourceServer.Databases['master'].ExecuteWithResults($sql) + # Same query as above, so it runs on the connection as well. Going through the + # master database would leave the connection of the caller there. See #10555. + $hashedPassDt = $sourceServer.ConnectionContext.ExecuteWithResults($sql) $hashedPass = $hashedPassDt.Tables[0].Rows[0].Item(0) } diff --git a/public/Remove-DbaAgentJob.ps1 b/public/Remove-DbaAgentJob.ps1 index 3d1c0663493a..35748cf1f499 100644 --- a/public/Remove-DbaAgentJob.ps1 +++ b/public/Remove-DbaAgentJob.ps1 @@ -136,8 +136,10 @@ function Remove-DbaAgentJob { $dropSchedule = 0 } Write-Message -Level SomewhatVerbose -Message "Removing job" - $dropJobQuery = ("EXEC dbo.sp_delete_job @job_name = '{0}', @delete_history = {1}, @delete_unused_schedule = {2}" -f $currentJob.Name.Replace("'", "''"), $dropHistory, $dropSchedule) - $server.Databases['msdb'].ExecuteNonQuery($dropJobQuery) + # The procedure is named in full so that it does not need the connection to be in msdb. + # Going through the msdb database would leave the connection of the caller there. See #10555. + $dropJobQuery = ("EXEC msdb.dbo.sp_delete_job @job_name = '{0}', @delete_history = {1}, @delete_unused_schedule = {2}" -f $currentJob.Name.Replace("'", "''"), $dropHistory, $dropSchedule) + $server.ConnectionContext.ExecuteNonQuery($dropJobQuery) $server.JobServer.Jobs.Refresh() Remove-TeppCacheItem -SqlInstance $server -Type job -Name $currentJob.Name [PSCustomObject]@{ diff --git a/public/Set-DbaTempDbConfig.ps1 b/public/Set-DbaTempDbConfig.ps1 index 669f282061bc..9671f622c2de 100644 --- a/public/Set-DbaTempDbConfig.ps1 +++ b/public/Set-DbaTempDbConfig.ps1 @@ -358,7 +358,10 @@ ORDER BY file_id; } else { if ($Pscmdlet.ShouldProcess($instance, "Executing query and informing that a restart is required.")) { try { - $server.Databases['master'].ExecuteNonQuery($sql) + # ALTER DATABASE does not depend on the current database, so this runs on the + # connection. Going through the master database would leave the connection of the + # caller there. See #10555. + $server.ConnectionContext.ExecuteNonQuery($sql) Write-Message -Level Verbose -Message "tempdb successfully reconfigured." [PSCustomObject]@{ diff --git a/tests/Get-DbaDbDetachedFileInfo.Tests.ps1 b/tests/Get-DbaDbDetachedFileInfo.Tests.ps1 index 247cc631837d..f8e8cba6925f 100644 --- a/tests/Get-DbaDbDetachedFileInfo.Tests.ps1 +++ b/tests/Get-DbaDbDetachedFileInfo.Tests.ps1 @@ -64,4 +64,38 @@ Describe $CommandName -Tag IntegrationTests { $results.LogFiles | Should -Not -BeNullOrEmpty } } + + Context "The connection of the caller keeps its database (#10555)" { + BeforeAll { + $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + + # The collation lookup used to run through the master database, which leaves the connection of + # the caller there. Only a non-pooled connection shows it, because SMO reopens a pooled one at + # its default database. + $callerServer = Connect-DbaInstance -SqlInstance $TestConfig.InstanceSingle -Database msdb -NonPooledConnection + $callerResult = Get-DbaDbDetachedFileInfo -SqlInstance $callerServer -Path $path + + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") + } + + AfterAll { + $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + + $null = $callerServer | Disconnect-DbaInstance + + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") + } + + It "still reads the detached file" { + $callerResult.Name | Should -Be $dbname + } + + It "still resolves the collation" { + $callerResult.Collation | Should -Not -BeNullOrEmpty + } + + It "leaves the connection in the database it was on" { + $callerServer.ConnectionContext.ExecuteScalar("SELECT DB_NAME()") | Should -Be "msdb" + } + } } \ No newline at end of file diff --git a/tests/Remove-DbaAgentJob.Tests.ps1 b/tests/Remove-DbaAgentJob.Tests.ps1 index d2836b6ccf4d..88baa3ebe469 100644 --- a/tests/Remove-DbaAgentJob.Tests.ps1 +++ b/tests/Remove-DbaAgentJob.Tests.ps1 @@ -159,4 +159,38 @@ Describe $CommandName -Tag IntegrationTests { (Get-DbaAgentJob -SqlInstance $TestConfig.InstanceSingle -Job dbatoolsci_testjob_validation) | Should -Not -BeNullOrEmpty } } + + Context "The connection of the caller keeps its database (#10555)" { + BeforeAll { + $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + + $contextJobName = "dbatoolsci_testjob_context" + $null = New-DbaAgentJob -SqlInstance $TestConfig.InstanceSingle -Job $contextJobName + + # sp_delete_job used to be run through the msdb database, which leaves the connection of the + # caller there. Only a non-pooled connection shows it, because SMO reopens a pooled one at its + # default database. + $callerServer = Connect-DbaInstance -SqlInstance $TestConfig.InstanceSingle -NonPooledConnection + $null = Remove-DbaAgentJob -SqlInstance $callerServer -Job $contextJobName + + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") + } + + AfterAll { + $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + + $null = $callerServer | Disconnect-DbaInstance + $null = Get-DbaAgentJob -SqlInstance $TestConfig.InstanceSingle -Job $contextJobName | Remove-DbaAgentJob -ErrorAction SilentlyContinue + + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") + } + + It "still removes the job" { + Get-DbaAgentJob -SqlInstance $TestConfig.InstanceSingle -Job $contextJobName | Should -BeNullOrEmpty + } + + It "leaves the connection in the database it was on" { + $callerServer.ConnectionContext.ExecuteScalar("SELECT DB_NAME()") | Should -Be "master" + } + } } \ No newline at end of file