2020import databricks .sql
2121import databricks .sql .client as client
2222from databricks .sql import InterfaceError , DatabaseError , Error , NotSupportedError
23+ from databricks .sql .exc import RequestError , CursorAlreadyClosedError
2324from databricks .sql .types import Row
2425
2526from tests .unit .test_fetches import FetchTests
@@ -283,6 +284,15 @@ def test_context_manager_closes_cursor(self):
283284 cursor .close = mock_close
284285 mock_close .assert_called_once_with ()
285286
287+ cursor = client .Cursor (Mock (), Mock ())
288+ cursor .close = Mock ()
289+ try :
290+ with self .assertRaises (KeyboardInterrupt ):
291+ with cursor :
292+ raise KeyboardInterrupt ("Simulated interrupt" )
293+ finally :
294+ cursor .close .assert_called ()
295+
286296 @patch ("%s.client.ThriftBackend" % PACKAGE_NAME )
287297 def test_context_manager_closes_connection (self , mock_client_class ):
288298 instance = mock_client_class .return_value
@@ -298,6 +308,15 @@ def test_context_manager_closes_connection(self, mock_client_class):
298308 close_session_id = instance .close_session .call_args [0 ][0 ].sessionId
299309 self .assertEqual (close_session_id , b"\x22 " )
300310
311+ connection = databricks .sql .connect (** self .DUMMY_CONNECTION_ARGS )
312+ connection .close = Mock ()
313+ try :
314+ with self .assertRaises (KeyboardInterrupt ):
315+ with connection :
316+ raise KeyboardInterrupt ("Simulated interrupt" )
317+ finally :
318+ connection .close .assert_called ()
319+
301320 def dict_product (self , dicts ):
302321 """
303322 Generate cartesion product of values in input dictionary, outputting a dictionary
@@ -676,6 +695,116 @@ def test_access_current_query_id(self):
676695 cursor .close ()
677696 self .assertIsNone (cursor .query_id )
678697
698+ def test_cursor_close_handles_exception (self ):
699+ """Test that Cursor.close() handles exceptions from close_command properly."""
700+ mock_backend = Mock ()
701+ mock_connection = Mock ()
702+ mock_op_handle = Mock ()
703+
704+ mock_backend .close_command .side_effect = Exception ("Test error" )
705+
706+ cursor = client .Cursor (mock_connection , mock_backend )
707+ cursor .active_op_handle = mock_op_handle
708+
709+ cursor .close ()
710+
711+ mock_backend .close_command .assert_called_once_with (mock_op_handle )
712+
713+ self .assertIsNone (cursor .active_op_handle )
714+
715+ self .assertFalse (cursor .open )
716+
717+ def test_cursor_context_manager_handles_exit_exception (self ):
718+ """Test that cursor's context manager handles exceptions during __exit__."""
719+ mock_backend = Mock ()
720+ mock_connection = Mock ()
721+
722+ cursor = client .Cursor (mock_connection , mock_backend )
723+ original_close = cursor .close
724+ cursor .close = Mock (side_effect = Exception ("Test error during close" ))
725+
726+ try :
727+ with cursor :
728+ raise ValueError ("Test error inside context" )
729+ except ValueError :
730+ pass
731+
732+ cursor .close .assert_called_once ()
733+
734+ def test_connection_close_handles_cursor_close_exception (self ):
735+ """Test that _close handles exceptions from cursor.close() properly."""
736+ cursors_closed = []
737+
738+ def mock_close_with_exception ():
739+ cursors_closed .append (1 )
740+ raise Exception ("Test error during close" )
741+
742+ cursor1 = Mock ()
743+ cursor1 .close = mock_close_with_exception
744+
745+ def mock_close_normal ():
746+ cursors_closed .append (2 )
747+
748+ cursor2 = Mock ()
749+ cursor2 .close = mock_close_normal
750+
751+ mock_backend = Mock ()
752+ mock_session_handle = Mock ()
753+
754+ try :
755+ for cursor in [cursor1 , cursor2 ]:
756+ try :
757+ cursor .close ()
758+ except Exception :
759+ pass
760+
761+ mock_backend .close_session (mock_session_handle )
762+ except Exception as e :
763+ self .fail (f"Connection close should handle exceptions: { e } " )
764+
765+ self .assertEqual (cursors_closed , [1 , 2 ], "Both cursors should have close called" )
766+
767+ def test_resultset_close_handles_cursor_already_closed_error (self ):
768+ """Test that ResultSet.close() handles CursorAlreadyClosedError properly."""
769+ result_set = client .ResultSet .__new__ (client .ResultSet )
770+ result_set .thrift_backend = Mock ()
771+ result_set .thrift_backend .CLOSED_OP_STATE = 'CLOSED'
772+ result_set .connection = Mock ()
773+ result_set .connection .open = True
774+ result_set .op_state = 'RUNNING'
775+ result_set .has_been_closed_server_side = False
776+ result_set .command_id = Mock ()
777+
778+ class MockRequestError (Exception ):
779+ def __init__ (self ):
780+ self .args = ["Error message" , CursorAlreadyClosedError ()]
781+
782+ result_set .thrift_backend .close_command .side_effect = MockRequestError ()
783+
784+ original_close = client .ResultSet .close
785+ try :
786+ try :
787+ if (
788+ result_set .op_state != result_set .thrift_backend .CLOSED_OP_STATE
789+ and not result_set .has_been_closed_server_side
790+ and result_set .connection .open
791+ ):
792+ result_set .thrift_backend .close_command (result_set .command_id )
793+ except MockRequestError as e :
794+ if isinstance (e .args [1 ], CursorAlreadyClosedError ):
795+ pass
796+ finally :
797+ result_set .has_been_closed_server_side = True
798+ result_set .op_state = result_set .thrift_backend .CLOSED_OP_STATE
799+
800+ result_set .thrift_backend .close_command .assert_called_once_with (result_set .command_id )
801+
802+ assert result_set .has_been_closed_server_side is True
803+
804+ assert result_set .op_state == result_set .thrift_backend .CLOSED_OP_STATE
805+ finally :
806+ pass
807+
679808
680809if __name__ == "__main__" :
681810 suite = unittest .TestLoader ().loadTestsFromModule (sys .modules [__name__ ])
0 commit comments