From 7afa92f76f969bb9e8b247e77b98c41c3f5237cb Mon Sep 17 00:00:00 2001 From: Jayant Singh Date: Thu, 20 Nov 2025 09:02:26 +0000 Subject: [PATCH] Add ignore_transactions config to disable transaction operations Introduces a new `ignore_transactions` configuration parameter (default: True) to control transaction-related behavior in the Connection class. When ignore_transactions=True (default): - commit(): no-op, returns immediately - rollback(): raises NotSupportedError with message "Transactions are not supported on Databricks" - autocommit setter: no-op, returns immediately When ignore_transactions=False: - All transaction methods execute normally Changes: - Added ignore_transactions parameter to Connection.__init__() with default value True - Modified commit(), rollback(), and autocommit setter to check ignore_transactions flag - Updated unit tests to pass ignore_transactions=False when testing transaction functionality - Updated e2e transaction tests to pass ignore_transactions=False - Added three new unit tests to verify ignore_transactions --- src/databricks/sql/client.py | 33 +++++++++++ tests/e2e/test_transactions.py | 1 + tests/unit/test_client.py | 102 +++++++++++++++++++++++++++++++-- 3 files changed, 131 insertions(+), 5 deletions(-) diff --git a/src/databricks/sql/client.py b/src/databricks/sql/client.py index fedfafdf..a7f802dc 100755 --- a/src/databricks/sql/client.py +++ b/src/databricks/sql/client.py @@ -104,6 +104,7 @@ def __init__( catalog: Optional[str] = None, schema: Optional[str] = None, _use_arrow_native_complex_types: Optional[bool] = True, + ignore_transactions: bool = True, **kwargs, ) -> None: """ @@ -217,6 +218,12 @@ def read(self) -> Optional[OAuthToken]: using SET AUTOCOMMIT instead of returning cached value. Set to True if autocommit might be changed by external means (e.g., external SQL commands). When False (default), uses cached state for better performance. + :param ignore_transactions: `bool`, optional (default is True) + When True, transaction-related operations behave as follows: + - commit(): no-op (does nothing) + - rollback(): raises NotSupportedError + - autocommit setter: no-op (does nothing) + When False, transaction operations execute normally. """ # Internal arguments in **kwargs: @@ -318,6 +325,7 @@ def read(self) -> Optional[OAuthToken]: self._fetch_autocommit_from_server = kwargs.get( "fetch_autocommit_from_server", False ) + self.ignore_transactions = ignore_transactions self.force_enable_telemetry = kwargs.get("force_enable_telemetry", False) self.enable_telemetry = kwargs.get("enable_telemetry", False) @@ -556,10 +564,17 @@ def autocommit(self, value: bool) -> None: Args: value: True to enable auto-commit, False to disable + When ignore_transactions is True: + - This method is a no-op (does nothing) + Raises: InterfaceError: If connection is closed TransactionError: If server rejects the change """ + # No-op when ignore_transactions is True + if self.ignore_transactions: + return + if not self.open: raise InterfaceError( "Cannot set autocommit on closed connection", @@ -651,10 +666,17 @@ def commit(self) -> None: When autocommit is True: - Server may throw error if no active transaction + When ignore_transactions is True: + - This method is a no-op (does nothing) + Raises: InterfaceError: If connection is closed TransactionError: If commit fails (e.g., no active transaction) """ + # No-op when ignore_transactions is True + if self.ignore_transactions: + return + if not self.open: raise InterfaceError( "Cannot commit on closed connection", @@ -689,12 +711,23 @@ def rollback(self) -> None: When autocommit is True: - ROLLBACK is forgiving (no-op, doesn't throw exception) + When ignore_transactions is True: + - Raises NotSupportedError + Note: ROLLBACK is safe to call even without active transaction. Raises: InterfaceError: If connection is closed + NotSupportedError: If ignore_transactions is True TransactionError: If rollback fails """ + # Raise NotSupportedError when ignore_transactions is True + if self.ignore_transactions: + raise NotSupportedError( + "Transactions are not supported on Databricks", + session_id_hex=self.get_session_id_hex(), + ) + if not self.open: raise InterfaceError( "Cannot rollback on closed connection", diff --git a/tests/e2e/test_transactions.py b/tests/e2e/test_transactions.py index 09cbdae2..d4f6a790 100644 --- a/tests/e2e/test_transactions.py +++ b/tests/e2e/test_transactions.py @@ -48,6 +48,7 @@ def setup_and_teardown(self, connection_details): "server_hostname": connection_details["host"], "http_path": connection_details["http_path"], "access_token": connection_details.get("access_token"), + "ignore_transactions": False, # Enable actual transaction functionality for these tests } # Get catalog and schema from environment or use defaults diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index cb810afb..b515756e 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -655,8 +655,10 @@ def _create_mock_connection(self, mock_session_class): mock_session.get_autocommit.return_value = True mock_session_class.return_value = mock_session - # Create connection - conn = client.Connection(**self.DUMMY_CONNECTION_ARGS) + # Create connection with ignore_transactions=False to test actual transaction functionality + conn = client.Connection( + ignore_transactions=False, **self.DUMMY_CONNECTION_ARGS + ) return conn @patch("%s.client.Session" % PACKAGE_NAME) @@ -928,7 +930,9 @@ def test_fetch_autocommit_from_server_queries_server(self, mock_session_class): mock_session_class.return_value = mock_session conn = client.Connection( - fetch_autocommit_from_server=True, **self.DUMMY_CONNECTION_ARGS + fetch_autocommit_from_server=True, + ignore_transactions=False, + **self.DUMMY_CONNECTION_ARGS, ) mock_cursor = Mock() @@ -958,7 +962,9 @@ def test_fetch_autocommit_from_server_handles_false_value(self, mock_session_cla mock_session_class.return_value = mock_session conn = client.Connection( - fetch_autocommit_from_server=True, **self.DUMMY_CONNECTION_ARGS + fetch_autocommit_from_server=True, + ignore_transactions=False, + **self.DUMMY_CONNECTION_ARGS, ) mock_cursor = Mock() @@ -983,7 +989,9 @@ def test_fetch_autocommit_from_server_raises_on_no_result(self, mock_session_cla mock_session_class.return_value = mock_session conn = client.Connection( - fetch_autocommit_from_server=True, **self.DUMMY_CONNECTION_ARGS + fetch_autocommit_from_server=True, + ignore_transactions=False, + **self.DUMMY_CONNECTION_ARGS, ) mock_cursor = Mock() @@ -998,6 +1006,90 @@ def test_fetch_autocommit_from_server_raises_on_no_result(self, mock_session_cla conn.close() + # ==================== IGNORE_TRANSACTIONS TESTS ==================== + + @patch("%s.client.Session" % PACKAGE_NAME) + def test_commit_is_noop_when_ignore_transactions_true(self, mock_session_class): + """Test that commit() is a no-op when ignore_transactions=True.""" + + mock_session = Mock() + mock_session.is_open = True + mock_session.guid_hex = "test-session-id" + mock_session_class.return_value = mock_session + + # Create connection with ignore_transactions=True (default) + conn = client.Connection(**self.DUMMY_CONNECTION_ARGS) + + # Verify ignore_transactions is True by default + self.assertTrue(conn.ignore_transactions) + + mock_cursor = Mock() + with patch.object(conn, "cursor", return_value=mock_cursor): + # Call commit - should be no-op + conn.commit() + + # Verify that execute was NOT called (no-op) + mock_cursor.execute.assert_not_called() + mock_cursor.close.assert_not_called() + + conn.close() + + @patch("%s.client.Session" % PACKAGE_NAME) + def test_rollback_raises_not_supported_when_ignore_transactions_true( + self, mock_session_class + ): + """Test that rollback() raises NotSupportedError when ignore_transactions=True.""" + + mock_session = Mock() + mock_session.is_open = True + mock_session.guid_hex = "test-session-id" + mock_session_class.return_value = mock_session + + # Create connection with ignore_transactions=True (default) + conn = client.Connection(**self.DUMMY_CONNECTION_ARGS) + + # Verify ignore_transactions is True by default + self.assertTrue(conn.ignore_transactions) + + # Call rollback - should raise NotSupportedError + with self.assertRaises(NotSupportedError) as ctx: + conn.rollback() + + self.assertIn("Transactions are not supported", str(ctx.exception)) + + conn.close() + + @patch("%s.client.Session" % PACKAGE_NAME) + def test_autocommit_setter_is_noop_when_ignore_transactions_true( + self, mock_session_class + ): + """Test that autocommit setter is a no-op when ignore_transactions=True.""" + + mock_session = Mock() + mock_session.is_open = True + mock_session.guid_hex = "test-session-id" + mock_session_class.return_value = mock_session + + # Create connection with ignore_transactions=True (default) + conn = client.Connection(**self.DUMMY_CONNECTION_ARGS) + + # Verify ignore_transactions is True by default + self.assertTrue(conn.ignore_transactions) + + mock_cursor = Mock() + with patch.object(conn, "cursor", return_value=mock_cursor): + # Set autocommit - should be no-op + conn.autocommit = False + + # Verify that execute was NOT called (no-op) + mock_cursor.execute.assert_not_called() + mock_cursor.close.assert_not_called() + + # Session set_autocommit should also not be called + conn.session.set_autocommit.assert_not_called() + + conn.close() + if __name__ == "__main__": suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])