20
20
import databricks .sql
21
21
import databricks .sql .client as client
22
22
from databricks .sql import InterfaceError , DatabaseError , Error , NotSupportedError
23
+ from databricks .sql .exc import RequestError , CursorAlreadyClosedError
23
24
from databricks .sql .types import Row
24
25
25
26
from tests .unit .test_fetches import FetchTests
@@ -283,6 +284,15 @@ def test_context_manager_closes_cursor(self):
283
284
cursor .close = mock_close
284
285
mock_close .assert_called_once_with ()
285
286
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
+
286
296
@patch ("%s.client.ThriftBackend" % PACKAGE_NAME )
287
297
def test_context_manager_closes_connection (self , mock_client_class ):
288
298
instance = mock_client_class .return_value
@@ -298,6 +308,15 @@ def test_context_manager_closes_connection(self, mock_client_class):
298
308
close_session_id = instance .close_session .call_args [0 ][0 ].sessionId
299
309
self .assertEqual (close_session_id , b"\x22 " )
300
310
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
+
301
320
def dict_product (self , dicts ):
302
321
"""
303
322
Generate cartesion product of values in input dictionary, outputting a dictionary
@@ -676,6 +695,116 @@ def test_access_current_query_id(self):
676
695
cursor .close ()
677
696
self .assertIsNone (cursor .query_id )
678
697
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
+
679
808
680
809
if __name__ == "__main__" :
681
810
suite = unittest .TestLoader ().loadTestsFromModule (sys .modules [__name__ ])
0 commit comments