@@ -640,6 +640,148 @@ async def failing_execute(sql, params=None):
640640
641641 await pool .close ()
642642
643+ async def test_escaped_reference_rejected_after_release (self ) -> None :
644+ """Using a connection after it's returned to the pool must raise InterfaceError."""
645+ import asyncio
646+
647+ from dqliteclient .connection import DqliteConnection
648+ from dqliteclient .exceptions import InterfaceError
649+
650+ pool = ConnectionPool (["localhost:9001" ], max_size = 1 )
651+
652+ real_conn = DqliteConnection ("127.0.0.1:9001" )
653+ real_conn ._protocol = MagicMock ()
654+ real_conn ._db_id = 1
655+ real_conn ._bound_loop = asyncio .get_running_loop ()
656+ real_conn ._protocol .exec_sql = AsyncMock (return_value = (0 , 1 ))
657+ real_conn ._protocol .query_sql = AsyncMock (return_value = (["id" ], [[1 ]]))
658+ real_conn ._protocol .close = MagicMock ()
659+ real_conn ._protocol .wait_closed = AsyncMock ()
660+
661+ with patch .object (pool ._cluster , "connect" , return_value = real_conn ):
662+ await pool .initialize ()
663+
664+ # Stash a reference to the connection
665+ escaped = None
666+ async with pool .acquire () as conn :
667+ escaped = conn
668+ await conn .execute ("SELECT 1" )
669+
670+ # Connection is now back in the pool — escaped reference must be rejected
671+ assert escaped is not None
672+ with pytest .raises (InterfaceError , match = "returned to the pool" ):
673+ await escaped .execute ("SELECT 1" )
674+
675+ await pool .close ()
676+
677+ async def test_escaped_reference_rejected_after_exception (self ) -> None :
678+ """Escaped reference must be rejected even when user code raises."""
679+ import asyncio
680+
681+ from dqliteclient .connection import DqliteConnection
682+ from dqliteclient .exceptions import InterfaceError
683+
684+ pool = ConnectionPool (["localhost:9001" ], max_size = 1 )
685+
686+ real_conn = DqliteConnection ("127.0.0.1:9001" )
687+ real_conn ._protocol = MagicMock ()
688+ real_conn ._db_id = 1
689+ real_conn ._bound_loop = asyncio .get_running_loop ()
690+ real_conn ._protocol .exec_sql = AsyncMock (return_value = (0 , 1 ))
691+ real_conn ._protocol .close = MagicMock ()
692+ real_conn ._protocol .wait_closed = AsyncMock ()
693+
694+ with patch .object (pool ._cluster , "connect" , return_value = real_conn ):
695+ await pool .initialize ()
696+
697+ escaped = None
698+ with pytest .raises (ValueError , match = "app error" ):
699+ async with pool .acquire () as conn :
700+ escaped = conn
701+ raise ValueError ("app error" )
702+
703+ assert escaped is not None
704+ with pytest .raises (InterfaceError , match = "returned to the pool" ):
705+ await escaped .execute ("SELECT 1" )
706+
707+ await pool .close ()
708+
709+ async def test_pool_release_rolls_back_transaction_with_real_connection (self ) -> None :
710+ """_reset_connection must be able to ROLLBACK before _pool_released is set."""
711+ import asyncio
712+
713+ from dqliteclient .connection import DqliteConnection
714+
715+ pool = ConnectionPool (["localhost:9001" ], max_size = 1 )
716+
717+ real_conn = DqliteConnection ("127.0.0.1:9001" )
718+ real_conn ._protocol = MagicMock ()
719+ real_conn ._db_id = 1
720+ real_conn ._bound_loop = asyncio .get_running_loop ()
721+ real_conn ._protocol .exec_sql = AsyncMock (return_value = (0 , 0 ))
722+ real_conn ._protocol .close = MagicMock ()
723+ real_conn ._protocol .wait_closed = AsyncMock ()
724+
725+ with patch .object (pool ._cluster , "connect" , return_value = real_conn ):
726+ await pool .initialize ()
727+
728+ # Simulate a connection with an open transaction returned to pool
729+ async with pool .acquire () as conn :
730+ conn ._in_transaction = True
731+
732+ # The pool should have issued ROLLBACK successfully (not destroyed the conn)
733+ assert not real_conn ._in_transaction
734+ # Connection should be back in the pool (not destroyed)
735+ assert pool ._pool .qsize () == 1
736+ assert pool ._size == 1
737+
738+ await pool .close ()
739+
740+ async def test_escaped_reference_works_when_reacquired (self ) -> None :
741+ """A connection re-acquired from the pool must work normally."""
742+ import asyncio
743+
744+ from dqliteclient .connection import DqliteConnection
745+
746+ pool = ConnectionPool (["localhost:9001" ], max_size = 1 )
747+
748+ real_conn = DqliteConnection ("127.0.0.1:9001" )
749+ real_conn ._protocol = MagicMock ()
750+ real_conn ._db_id = 1
751+ real_conn ._bound_loop = asyncio .get_running_loop ()
752+ real_conn ._protocol .exec_sql = AsyncMock (return_value = (0 , 1 ))
753+ real_conn ._protocol .close = MagicMock ()
754+ real_conn ._protocol .wait_closed = AsyncMock ()
755+
756+ with patch .object (pool ._cluster , "connect" , return_value = real_conn ):
757+ await pool .initialize ()
758+
759+ # First acquire and release
760+ async with pool .acquire () as conn :
761+ await conn .execute ("SELECT 1" )
762+
763+ # Second acquire — same connection from pool must work
764+ async with pool .acquire () as conn :
765+ await conn .execute ("SELECT 2" )
766+
767+ await pool .close ()
768+
769+ async def test_standalone_connection_not_affected_by_pool_guard (self ) -> None :
770+ """A DqliteConnection used standalone (not from a pool) must not be affected."""
771+ import asyncio
772+
773+ from dqliteclient .connection import DqliteConnection
774+
775+ conn = DqliteConnection ("127.0.0.1:9001" )
776+ conn ._protocol = MagicMock ()
777+ conn ._db_id = 1
778+ conn ._bound_loop = asyncio .get_running_loop ()
779+ conn ._protocol .exec_sql = AsyncMock (return_value = (0 , 1 ))
780+
781+ # Standalone connection — no pool involved, should work fine
782+ await conn .execute ("SELECT 1" )
783+ await conn .execute ("SELECT 2" ) # No error — _pool_released is always False
784+
643785
644786class TestConnectionPoolIntegration :
645787 """Integration tests requiring mocked connections."""
0 commit comments