2222from databricks .sql .result_set import ResultSet , ThriftResultSet
2323from databricks .sql .backend .types import CommandId , CommandState , SessionId , BackendType
2424
25+ try :
26+ from thrift .protocol import fastbinary # noqa: F401
27+
28+ _HAS_THRIFT_FASTBINARY = True
29+ except ImportError :
30+ _HAS_THRIFT_FASTBINARY = False
31+
2532
2633def retry_policy_factory ():
2734 return { # (type, default, min, max)
@@ -386,19 +393,13 @@ def _http_client_with_reply(reply_bytes):
386393 http_client ._THttpClient__pool .request .return_value = mock_resp
387394 return http_client , mock_resp
388395
389- def test_http_client_buffers_reply_and_decodes_with_accelerated_protocol (self ):
390- """The reply body is read from urllib3 once, then decoded from memory,
391- including by the C-accelerated protocol (the transport is a
392- CReadableTransport) for payloads above thrift's ~15.6 MiB default limit."""
396+ @staticmethod
397+ def _fetch_results_reply (batch ):
398+ """Wire bytes of a FetchResults REPLY carrying one inline Arrow batch."""
393399 from thrift .protocol import TBinaryProtocol
394400 from thrift .transport import TTransport
395401 from databricks .sql .thrift_api .TCLIService import TCLIService
396- from databricks .sql .backend .thrift_backend import (
397- THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT ,
398- THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT ,
399- )
400402
401- batch = b"x" * (20 * 1024 * 1024 )
402403 resp = ttypes .TFetchResultsResp (
403404 status = ttypes .TStatus (statusCode = ttypes .TStatusCode .SUCCESS_STATUS ),
404405 hasMoreRows = False ,
@@ -413,31 +414,71 @@ def test_http_client_buffers_reply_and_decodes_with_accelerated_protocol(self):
413414 reply_proto .writeMessageBegin ("FetchResults" , 2 , 0 ) # REPLY
414415 TCLIService .FetchResults_result (success = resp ).write (reply_proto )
415416 reply_proto .writeMessageEnd ()
417+ return reply .getvalue ()
418+
419+ @staticmethod
420+ def _fetch_results_req ():
421+ op_handle = ttypes .TOperationHandle (
422+ operationId = ttypes .THandleIdentifier (guid = b"g" * 16 , secret = b"s" * 16 ),
423+ operationType = ttypes .TOperationType .EXECUTE_STATEMENT ,
424+ hasResultSet = True ,
425+ )
426+ return ttypes .TFetchResultsReq (
427+ operationHandle = op_handle , orientation = 0 , maxRows = 1
428+ )
429+
430+ def _accelerated_client (self , http_client ):
431+ from thrift .protocol import TBinaryProtocol
432+ from databricks .sql .thrift_api .TCLIService import TCLIService
433+ from databricks .sql .backend .thrift_backend import (
434+ THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT ,
435+ THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT ,
436+ )
416437
417- http_client , mock_resp = self ._http_client_with_reply (reply .getvalue ())
418- self .assertIsInstance (http_client , TTransport .CReadableTransport )
419438 protocol = TBinaryProtocol .TBinaryProtocolAccelerated (
420439 http_client ,
421440 string_length_limit = THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT ,
422441 container_length_limit = THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT ,
423442 )
424- client = TCLIService .Client (protocol )
443+ return protocol , TCLIService .Client (protocol )
425444
426- op_handle = ttypes .TOperationHandle (
427- operationId = ttypes .THandleIdentifier (guid = b"g" * 16 , secret = b"s" * 16 ),
428- operationType = ttypes .TOperationType .EXECUTE_STATEMENT ,
429- hasResultSet = True ,
430- )
431- result = client .FetchResults (
432- ttypes .TFetchResultsReq (operationHandle = op_handle , orientation = 0 , maxRows = 1 )
445+ def test_http_client_buffers_reply_and_decodes_with_accelerated_protocol (self ):
446+ """The reply body is read from urllib3 once and decoded from memory via
447+ TBinaryProtocolAccelerated, for payloads above thrift's ~15.6 MiB
448+ default limit. Runs on both the C and the pure-Python fallback path."""
449+ from thrift .transport import TTransport
450+
451+ batch = b"x" * (20 * 1024 * 1024 )
452+ http_client , mock_resp = self ._http_client_with_reply (
453+ self ._fetch_results_reply (batch )
433454 )
455+ self .assertIsInstance (http_client , TTransport .CReadableTransport )
456+ _ , client = self ._accelerated_client (http_client )
457+
458+ result = client .FetchResults (self ._fetch_results_req ())
434459
435460 self .assertEqual (result .results .arrowBatches [0 ].batch , batch )
436461 mock_resp .read .assert_called_once_with ()
437462
438463 http_client .close ()
439464 self .assertEqual (http_client .read (1 ), b"" )
440465
466+ @unittest .skipUnless (
467+ _HAS_THRIFT_FASTBINARY , "thrift C extension (fastbinary) not built"
468+ )
469+ def test_http_client_reply_is_decoded_by_thrift_c_extension (self ):
470+ """TBinaryProtocolAccelerated silently falls back to pure Python without
471+ fastbinary, so assert the C decoder actually ran on the buffered body."""
472+ batch = b"x" * (20 * 1024 * 1024 )
473+ http_client , _ = self ._http_client_with_reply (self ._fetch_results_reply (batch ))
474+ protocol , client = self ._accelerated_client (http_client )
475+ protocol ._fast_decode = Mock (wraps = protocol ._fast_decode )
476+
477+ result = client .FetchResults (self ._fetch_results_req ())
478+
479+ self .assertEqual (result .results .arrowBatches [0 ].batch , batch )
480+ protocol ._fast_decode .assert_called ()
481+
441482 def test_http_client_cstringio_refill_on_truncated_reply_raises (self ):
442483 http_client , _ = self ._http_client_with_reply (b"" )
443484 with self .assertRaises (EOFError ):
@@ -446,26 +487,7 @@ def test_http_client_cstringio_refill_on_truncated_reply_raises(self):
446487 def test_reply_buffer_is_released_after_successful_make_request (self ):
447488 """make_request closes the transport on success too, so the buffered
448489 reply body is not kept on the connection after it has been decoded."""
449- from thrift .protocol import TBinaryProtocol
450- from thrift .transport import TTransport
451- from databricks .sql .thrift_api .TCLIService import TCLIService
452-
453490 batch = b"x" * (1024 * 1024 )
454- resp = ttypes .TFetchResultsResp (
455- status = ttypes .TStatus (statusCode = ttypes .TStatusCode .SUCCESS_STATUS ),
456- hasMoreRows = False ,
457- results = ttypes .TRowSet (
458- startRowOffset = 0 ,
459- rows = [],
460- arrowBatches = [ttypes .TSparkArrowBatch (batch = batch , rowCount = 1 )],
461- ),
462- )
463- reply = TTransport .TMemoryBuffer ()
464- reply_proto = TBinaryProtocol .TBinaryProtocol (reply )
465- reply_proto .writeMessageBegin ("FetchResults" , 2 , 0 ) # REPLY
466- TCLIService .FetchResults_result (success = resp ).write (reply_proto )
467- reply_proto .writeMessageEnd ()
468-
469491 thrift_backend = ThriftDatabricksClient (
470492 "foobar" ,
471493 443 ,
@@ -476,20 +498,12 @@ def test_reply_buffer_is_released_after_successful_make_request(self):
476498 http_client = MagicMock (),
477499 )
478500 mock_resp = Mock (status = 200 , reason = "OK" , headers = {})
479- mock_resp .read .return_value = reply . getvalue ( )
501+ mock_resp .read .return_value = self . _fetch_results_reply ( batch )
480502 thrift_backend ._transport ._THttpClient__pool = Mock ()
481503 thrift_backend ._transport ._THttpClient__pool .request .return_value = mock_resp
482504
483- op_handle = ttypes .TOperationHandle (
484- operationId = ttypes .THandleIdentifier (guid = b"g" * 16 , secret = b"s" * 16 ),
485- operationType = ttypes .TOperationType .EXECUTE_STATEMENT ,
486- hasResultSet = True ,
487- )
488505 result = thrift_backend .make_request (
489- thrift_backend ._client .FetchResults ,
490- ttypes .TFetchResultsReq (
491- operationHandle = op_handle , orientation = 0 , maxRows = 1
492- ),
506+ thrift_backend ._client .FetchResults , self ._fetch_results_req ()
493507 )
494508
495509 self .assertEqual (result .results .arrowBatches [0 ].batch , batch )
0 commit comments