@@ -393,9 +393,7 @@ def fake_session(**kw):
393393
394394
395395@pytest .mark .parametrize ("enabled" , [True , False ])
396- def test_open_session_passes_geospatial_representation_to_kernel (
397- monkeypatch , enabled
398- ):
396+ def test_open_session_passes_geospatial_representation_to_kernel (monkeypatch , enabled ):
399397 captured = {}
400398
401399 def fake_session (* , enable_geospatial_support = True , ** kw ):
@@ -807,70 +805,43 @@ def test_execute_command_forwards_row_limit(row_limit):
807805 stmt .set_row_limit .assert_called_once_with (row_limit )
808806
809807
810- # ---------------------------------------------------------------------------
811- # Staging / volume operations — fail loud (not silently no-op)
812- # ---------------------------------------------------------------------------
813-
814-
815808@pytest .mark .parametrize (
816- "operation " ,
809+ "allowed, expected " ,
817810 [
818- "PUT '/local/f.csv' INTO '/Volumes/c/s/v/f.csv'" ,
819- " put '/local/f' into '/Volumes/...'" , # leading ws + lowercase
820- "GET '/Volumes/c/s/v/f' TO '/local/f'" ,
821- "REMOVE '/Volumes/c/s/v/f'" ,
811+ (None , []),
812+ ("/tmp/files" , ["/tmp/files" ]),
813+ (["/tmp/a" , "/tmp/b" ], ["/tmp/a" , "/tmp/b" ]),
822814 ],
823815)
824- def test_staging_operation_raises_not_supported (operation ):
825- """Volume/staging PUT/GET/REMOVE must FAIL LOUD on the kernel path
826- (the kernel can't perform the presigned-URL transfer; silently
827- no-opping would make ETL ingest stale/missing data)."""
816+ @pytest .mark .parametrize ("async_op" , [False , True ])
817+ def test_execute_command_forwards_allowed_local_paths (allowed , expected , async_op ):
828818 c = _make_client ()
829819 c ._kernel_session = MagicMock ()
830820 cursor = MagicMock ()
821+ cursor .connection .staging_allowed_local_path = allowed
831822 cursor .arraysize = 100
832823 cursor .buffer_size_bytes = 1024
833-
834- with pytest .raises (NotSupportedError , match = "staging" ):
835- c .execute_command (
836- operation = operation ,
837- session_id = MagicMock (),
838- max_rows = 1 ,
839- max_bytes = 1 ,
840- lz4_compression = False ,
841- cursor = cursor ,
842- use_cloud_fetch = False ,
843- parameters = [],
844- async_op = False ,
845- enforce_embedded_schema_correctness = False ,
846- )
847-
848-
849- @pytest .mark .parametrize (
850- "operation, is_staging" ,
851- [
852- ("PUT '/f' INTO '/v'" , True ),
853- ("get '/v' to '/f'" , True ),
854- ("REMOVE '/v'" , True ),
855- # Comment-prefixed staging ops MUST still be caught — otherwise
856- # they slip into the silent-no-op bug this guard exists to close
857- # (regression: review #1 on PR #825). ETL scripts commonly
858- # prefix statements with comments.
859- ("-- upload the file\n PUT '/f' INTO '/v'" , True ),
860- ("/* staging */ PUT '/f' INTO '/v'" , True ),
861- ("/* c1 */\n -- c2\n get '/v' to '/f'" , True ), # mixed, multiple
862- (" \n \t PUT '/f' INTO '/v'" , True ), # leading whitespace only
863- ("SELECT 'GET' AS x" , False ), # word appears but not leading verb
864- ("SELECT * FROM puts" , False ),
865- ("-- PUT in a comment\n SELECT 1" , False ), # verb only in comment
866- ("/* PUT */ SELECT 1" , False ),
867- ("INSERT INTO t VALUES (1)" , False ),
868- ("" , False ),
869- ("-- just a comment" , False ), # comment only, no statement
870- ],
871- )
872- def test_is_staging_statement (operation , is_staging ):
873- assert kernel_client ._is_staging_statement (operation ) is is_staging
824+ stmt = c ._kernel_session .statement .return_value
825+ executed = MagicMock (
826+ statement_id = "00000000-0000-0000-0000-000000000001" ,
827+ arrow_schema = MagicMock (return_value = pa .schema ([("x" , pa .int64 ())])),
828+ )
829+ stmt .execute .return_value = executed
830+ stmt .submit .return_value = executed
831+ c .execute_command (
832+ operation = "PUT '/tmp/files/input' INTO '/Volumes/c/s/v/input'" ,
833+ session_id = MagicMock (),
834+ max_rows = 1 ,
835+ max_bytes = 1 ,
836+ lz4_compression = False ,
837+ cursor = cursor ,
838+ use_cloud_fetch = False ,
839+ parameters = [],
840+ async_op = async_op ,
841+ enforce_embedded_schema_correctness = False ,
842+ )
843+ stmt .set_staging_allowed_local_paths .assert_called_once_with (expected )
844+ (stmt .submit if async_op else stmt .execute ).assert_called_once ()
874845
875846
876847# ---------------------------------------------------------------------------
@@ -2582,3 +2553,21 @@ def test_only_reserved_headers_omits_kwarg(self, monkeypatch):
25822553 monkeypatch , [("Authorization" , "Bearer x" ), ("x-databricks-org-id" , "1" )]
25832554 )
25842555 assert "http_headers" not in captured
2556+
2557+
2558+ @pytest .mark .parametrize (
2559+ "code, http_status, expected" ,
2560+ [
2561+ ("PermissionDenied" , None , ProgrammingError ),
2562+ ("NotFound" , None , FileNotFoundError ),
2563+ ("NotFound" , 404 , OperationalError ),
2564+ ("NetworkError" , None , OperationalError ),
2565+ ],
2566+ )
2567+ def test_file_transfer_error_matches_thrift_exception (code , http_status , expected ):
2568+ error = _FakeKernelError (code = code , message = "File transfer failed" )
2569+ error .error_code = "FILE_TRANSFER_ERROR"
2570+ error .http_status = http_status
2571+ result = kernel_client ._reraise_kernel_error (error )
2572+ assert type (result ) is expected
2573+ assert result .error_code == "FILE_TRANSFER_ERROR"
0 commit comments