You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Eight sites in public/ and private/ close a connection the caller owns. Invoke-DbaQuery is the one that started this and is described in full first; the complete inventory follows below, together with the places that already get it right.
The damage is invisible on a pooled connection - SMO transparently reopens it from the pool. It is real on anything session-scoped: -NonPooledConnection, a dedicated admin connection, temp tables, SET options, session context, sp_getapplock. It also makes the database-context leak in #10555 look unreproducible, because the accidental reconnect resets the current database.
Invoke-DbaQuery
When Invoke-DbaQuery decides it cannot reuse the passed-in SMO server object, it builds its own connection with NonPooledConnection = $true and closes it afterwards:
# if the given connection started out open, don't close it.if ($connDbaInstanceParams.NonPooledConnection-and-not$startedWithAnOpenConnection) {
$null=$server|Disconnect-DbaInstance-Verbose:$false
}
The assumption behind the comment does not hold: Connect-DbaInstance frequently hands back the same object that was passed in instead of a new connection, because it only copies the context when something actually differs (public/Connect-DbaInstance.ps1:699-745). In that case Invoke-DbaQuery disconnects the caller's connection.
Two ways into it, both verified on SQL Server 2022 with Connect-DbaInstance -NonPooledConnection:
-Database naming the database the connection is already on.$startedWithAnOpenConnection compares ConnectionContext.DatabaseName (public/Invoke-DbaQuery.ps1:503), which is empty for a connection built by Connect-DbaInstance without -Database, while Connect-DbaInstance compares ConnectionContext.CurrentDatabase (public/Connect-DbaInstance.ps1:712), which is master. The two disagree, so nothing is copied and the caller's connection is closed.
A -SqlCredential holding a Windows account. That sets ConnectAsUserName, which makes $startedWithAnOpenConnection false unconditionally (public/Invoke-DbaQuery.ps1:505). Then even Invoke-DbaQuerywithout-Database closes the caller's connection.
--- Windows auth (ConnectAsUserName []) ---
IsOpen before : True
IsOpen after Invoke-DbaQuery : True
IsOpen after -Database master : False
--- SqlCredential with a Windows account (ConnectAsUserName [Admin@DOMAIN]) ---
IsOpen before : True
IsOpen after Invoke-DbaQuery : False
IsOpen after -Database master : False
Session state is lost with it, because SMO transparently reopens the connection on next use:
$server=Connect-DbaInstance-SqlInstance $instance-NonPooledConnection
$null=$server.ConnectionContext.ExecuteNonQuery("CREATE TABLE #t (id int)")
$null=Invoke-DbaQuery-SqlInstance $server-Database master -Query "SELECT 1"$server.ConnectionContext.ExecuteScalar("SELECT COUNT(*) FROM #t")
# Invalid object name '#t'.
The three unguarded -NonPooledConnection sites carry the comment "Close non-pooled connection as this is not done automatically. If it is a reused Server SMO, connection will be opened again automatically on next request." - so the reopen was known; what the comment misses is that the session dies with it.
Invoke-DbaAdvancedRestore has the widest reach, because Restore-DbaDatabase connects once and passes that server object down. Note it connects with -Database master, so a caller object that is not on master gets copied first and is safe - it is the caller who is already on master whose connection gets closed.
Verified against SQL Server 2022 with Connect-DbaInstance -NonPooledConnection, using a temp table as the session marker and Get-DbaDatabase as a control:
private/functions/Invoke-DbaDbCorruption.ps1:159 and :168 disconnect unconditionally as well, but the function is private and effectively test-only.
Where this is already done right
These are the patterns to copy rather than invent something new:
$dacOpened - Export-DbaCredential, Export-DbaLinkedServer, Export-DbaInstance, Copy-DbaCredential, Copy-DbaLinkedServer, Copy-DbaDbMail, Invoke-DbaDbDecryptObject, Sync-DbaAvailabilityGroup, Start-DbaMigration. A caller-supplied dedicated admin connection is detected (Test-DacConnection / $dacConnected) and reused, and only a connection the command opened itself is closed.
Connect-DbaInstance's own internal retries and private/scripts/updateTeppAsync.ps1 disconnect connections they genuinely own and need no change.
History
The disconnect was added for #6210 (Invoke-DbaQuery leaking connections it created), and the ConnectAsUserName condition comes from #7725 / #8491. Both are right in themselves - the gap is only that "did I create this connection?" is inferred from parameters instead of from what Connect-DbaInstance actually returned.
Suggested fix
Import-DbaBinaryFile and Add-DbaRegServerGroup never open a connection, so for those the disconnect can simply go away.
The remaining six need to know whether the connection is theirs, and the rule is the same everywhere: a command may only close what it opened itself. Connect-DbaInstance already knows this - it sets $isNewConnection internally - so the choice is between
surfacing that, so any command can ask instead of infer, which also keeps the next command from getting it wrong, or
a per-command flag, hand-rolled six more times in the shape of the existing $dacOpened / $startedWithANonPooledConnection, or
an object identity check where the original input is still in scope, for example in Invoke-DbaQuery:
if ($connDbaInstanceParams.NonPooledConnection-and-not [object]::ReferenceEquals($server,$instance.InputObject)) {
$null=$server|Disconnect-DbaInstance-Verbose:$false
}
Option 1 looks like the better deal given that eight sites got this wrong independently.
While in there: the DatabaseName vs CurrentDatabase mismatch between Invoke-DbaQuery and Connect-DbaInstance is worth aligning on its own - it also causes an unnecessary second connection for -Database master on a connection that is already on master.
Test
Per tests/CLAUDE.md, one regression test per fixed command: connect with -NonPooledConnection, create a temp table, run the command against that server object, then assert $server.ConnectionContext.IsOpen is still $true and the temp table is still there. A pooled connection cannot show this - it passes either way.
This text was created by Claude and reviewed by Andreas Jordan.
Eight sites in
public/andprivate/close a connection the caller owns.Invoke-DbaQueryis the one that started this and is described in full first; the complete inventory follows below, together with the places that already get it right.The damage is invisible on a pooled connection - SMO transparently reopens it from the pool. It is real on anything session-scoped:
-NonPooledConnection, a dedicated admin connection, temp tables,SEToptions, session context,sp_getapplock. It also makes the database-context leak in #10555 look unreproducible, because the accidental reconnect resets the current database.Invoke-DbaQuery
When
Invoke-DbaQuerydecides it cannot reuse the passed-in SMO server object, it builds its own connection withNonPooledConnection = $trueand closes it afterwards:public/Invoke-DbaQuery.ps1:544-548The assumption behind the comment does not hold:
Connect-DbaInstancefrequently hands back the same object that was passed in instead of a new connection, because it only copies the context when something actually differs (public/Connect-DbaInstance.ps1:699-745). In that caseInvoke-DbaQuerydisconnects the caller's connection.Two ways into it, both verified on SQL Server 2022 with
Connect-DbaInstance -NonPooledConnection:-Databasenaming the database the connection is already on.$startedWithAnOpenConnectioncomparesConnectionContext.DatabaseName(public/Invoke-DbaQuery.ps1:503), which is empty for a connection built byConnect-DbaInstancewithout-Database, whileConnect-DbaInstancecomparesConnectionContext.CurrentDatabase(public/Connect-DbaInstance.ps1:712), which ismaster. The two disagree, so nothing is copied and the caller's connection is closed.-SqlCredentialholding a Windows account. That setsConnectAsUserName, which makes$startedWithAnOpenConnectionfalse unconditionally (public/Invoke-DbaQuery.ps1:505). Then evenInvoke-DbaQuerywithout-Databasecloses the caller's connection.Steps to Reproduce
Session state is lost with it, because SMO transparently reopens the connection on next use:
The same defect elsewhere
Invoke-DbaQuery.ps1:547Connect-DbaInstance -NonPooledConnectionGet-DbaDbExtentDiff.ps1:191Connect-DbaInstance -NonPooledConnectionInstall-DbaMaintenanceSolution.ps1:895Connect-DbaInstance -NonPooledConnectionUpdate-DbaMaintenanceSolution.ps1:185Connect-DbaInstance -NonPooledConnectionInvoke-DbaAdvancedRestore.ps1:601and:609Connect-DbaInstance(plain)Import-DbaBinaryFile.ps1:279$tbl.Parent.Parentfrom a piped-in tableAdd-DbaRegServerGroup.ps1:155$reggroup.ParentServerImport-DbaSpConfigure.ps1:243-246Connect-DbaInstance(plain)The three unguarded
-NonPooledConnectionsites carry the comment "Close non-pooled connection as this is not done automatically. If it is a reused Server SMO, connection will be opened again automatically on next request." - so the reopen was known; what the comment misses is that the session dies with it.Invoke-DbaAdvancedRestorehas the widest reach, becauseRestore-DbaDatabaseconnects once and passes that server object down. Note it connects with-Database master, so a caller object that is not onmastergets copied first and is safe - it is the caller who is already onmasterwhose connection gets closed.Verified against SQL Server 2022 with
Connect-DbaInstance -NonPooledConnection, using a temp table as the session marker andGet-DbaDatabaseas a control:private/functions/Invoke-DbaDbCorruption.ps1:159and:168disconnect unconditionally as well, but the function is private and effectively test-only.Where this is already done right
These are the patterns to copy rather than invent something new:
$dacOpened-Export-DbaCredential,Export-DbaLinkedServer,Export-DbaInstance,Copy-DbaCredential,Copy-DbaLinkedServer,Copy-DbaDbMail,Invoke-DbaDbDecryptObject,Sync-DbaAvailabilityGroup,Start-DbaMigration. A caller-supplied dedicated admin connection is detected (Test-DacConnection/$dacConnected) and reused, and only a connection the command opened itself is closed.$startedWithANonPooledConnection-Write-DbaDbTableData.ps1:227. The same command also restores the original database context at:807-810, which is prior art for Database-scoped SMO calls silently change the current database of the shared connection #10555.$Newconnection-private/functions/Test-DbaRestoreVersion.ps1:88.Connect-DbaInstance's own internal retries andprivate/scripts/updateTeppAsync.ps1disconnect connections they genuinely own and need no change.History
The disconnect was added for #6210 (
Invoke-DbaQueryleaking connections it created), and theConnectAsUserNamecondition comes from #7725 / #8491. Both are right in themselves - the gap is only that "did I create this connection?" is inferred from parameters instead of from whatConnect-DbaInstanceactually returned.Suggested fix
Import-DbaBinaryFileandAdd-DbaRegServerGroupnever open a connection, so for those the disconnect can simply go away.The remaining six need to know whether the connection is theirs, and the rule is the same everywhere: a command may only close what it opened itself.
Connect-DbaInstancealready knows this - it sets$isNewConnectioninternally - so the choice is between$dacOpened/$startedWithANonPooledConnection, orInvoke-DbaQuery:Option 1 looks like the better deal given that eight sites got this wrong independently.
While in there: the
DatabaseNamevsCurrentDatabasemismatch betweenInvoke-DbaQueryandConnect-DbaInstanceis worth aligning on its own - it also causes an unnecessary second connection for-Database masteron a connection that is already onmaster.Test
Per
tests/CLAUDE.md, one regression test per fixed command: connect with-NonPooledConnection, create a temp table, run the command against that server object, then assert$server.ConnectionContext.IsOpenis still$trueand the temp table is still there. A pooled connection cannot show this - it passes either way.This text was created by Claude and reviewed by Andreas Jordan.