@@ -596,3 +596,73 @@ def test_pool_pre_ping_with_closed_connection(connection_details):
596596
597597 # Cleanup
598598 engine .dispose ()
599+
600+
601+ def test_is_disconnect_handles_runtime_errors (db_engine ):
602+ """Test that is_disconnect() properly classifies disconnect errors during query execution.
603+
604+ This tests the reactive error handling (complementary to pool_pre_ping's proactive checking).
605+ When a connection fails DURING a query, is_disconnect() should recognize the error
606+ and tell SQLAlchemy to invalidate the connection.
607+ """
608+ from sqlalchemy import create_engine , text
609+ from sqlalchemy .exc import DBAPIError
610+
611+ engine = create_engine (
612+ db_engine .url ,
613+ pool_pre_ping = False , # Disabled - we want to test is_disconnect, not do_ping
614+ pool_size = 1 ,
615+ max_overflow = 0 ,
616+ )
617+
618+ # Step 1: Execute a successful query
619+ with engine .connect () as conn :
620+ result = conn .execute (text ("SELECT VERSION()" )).scalar ()
621+ assert result is not None
622+
623+ # Get session ID of working connection
624+ raw_conn = conn .connection .dbapi_connection
625+ session_id_1 = raw_conn .get_session_id_hex ()
626+ assert session_id_1 is not None
627+
628+ # Step 2: Manually close the connection to simulate server-side session expiration
629+ pooled_conn = engine .pool ._pool .queue [0 ]
630+ pooled_conn .driver_connection .close ()
631+
632+ # Step 3: Try to execute query on closed connection
633+ # This should:
634+ # 1. Fail with an exception
635+ # 2. is_disconnect() gets called by SQLAlchemy
636+ # 3. Returns True (recognizes it as disconnect error)
637+ # 4. SQLAlchemy invalidates the connection
638+ # 5. Next operation gets a fresh connection
639+
640+ # First query will fail because connection is closed
641+ try :
642+ with engine .connect () as conn :
643+ conn .execute (text ("SELECT VERSION()" )).scalar ()
644+ # If we get here without exception, the connection wasn't actually closed
645+ pytest .skip ("Connection wasn't properly closed - cannot test is_disconnect" )
646+ except DBAPIError as e :
647+ # Expected - connection was closed
648+ # is_disconnect() should have been called and returned True
649+ # This causes SQLAlchemy to invalidate the connection
650+ assert "closed" in str (e ).lower () or "invalid" in str (e ).lower ()
651+
652+ # Step 4: Next query should work because is_disconnect() invalidated the bad connection
653+ with engine .connect () as conn :
654+ result = conn .execute (text ("SELECT VERSION()" )).scalar ()
655+ assert result is not None
656+
657+ # Verify we got a NEW connection
658+ raw_conn = conn .connection .dbapi_connection
659+ session_id_2 = raw_conn .get_session_id_hex ()
660+ assert session_id_2 is not None
661+
662+ # Different session ID proves connection was invalidated and recreated
663+ assert session_id_1 != session_id_2 , (
664+ "is_disconnect() should have invalidated the bad connection, "
665+ "causing SQLAlchemy to create a new one with different session ID"
666+ )
667+
668+ engine .dispose ()
0 commit comments