Skip to content

Commit 43fedf4

Browse files
fix(volume): preserve OS errors and exact stream token matching
Signed-off-by: Aakash Saravanan <aakash.saravanan@databricks.com>
1 parent 70f6695 commit 43fedf4

3 files changed

Lines changed: 56 additions & 6 deletions

File tree

‎src/databricks/sql/backend/kernel/_errors.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
``OperationalError`` here.
1818
1919
Local missing transfer files use the built-in ``FileNotFoundError`` class;
20-
the OS ``errno`` and ``filename`` attributes are unavailable.
20+
the original OS error code is preserved when available. The ``filename``
21+
attribute is unavailable.
2122
2223
These primitives live in their own module so both ``client.py``
2324
(which orchestrates PyO3 calls) and ``result_set.py`` (which calls
@@ -41,6 +42,7 @@
4142
from __future__ import annotations
4243

4344
import logging
45+
import os
4446
from typing import Union
4547

4648
from databricks.sql.exc import (
@@ -160,19 +162,26 @@ def reraise_kernel_error(
160162
"diagnostic-info": getattr(exc, "diagnostic_info", None),
161163
}
162164
message = getattr(exc, "message", str(exc))
165+
os_error_code = getattr(exc, "os_error_code", None)
166+
if os_error_code is None:
167+
file_error_args = (message,)
168+
elif os.name == "nt":
169+
file_error_args = (0, message, None, os_error_code)
170+
else:
171+
file_error_args = (os_error_code, message)
163172
new: Union[Error, OSError]
164173
# Local missing files have no HTTP status; remote missing files carry 404.
165174
if (
166175
error_code == "FILE_TRANSFER_PERMISSION_ERROR"
167176
and getattr(exc, "http_status", None) is None
168177
):
169-
new = PermissionError(message)
178+
new = PermissionError(*file_error_args)
170179
elif (
171180
file_transfer
172181
and code == "NotFound"
173182
and getattr(exc, "http_status", None) is None
174183
):
175-
new = FileNotFoundError(message)
184+
new = FileNotFoundError(*file_error_args)
176185
else:
177186
new = cls(message, context)
178187

@@ -181,6 +190,7 @@ def reraise_kernel_error(
181190
"sql_state",
182191
"error_code",
183192
"vendor_code",
193+
"os_error_code",
184194
"http_status",
185195
"retryable",
186196
"query_id",
@@ -199,8 +209,8 @@ def wrap_kernel_exception(what: str, exc: BaseException) -> "Union[Error, OSErro
199209
"""Map a PyO3 exception to the connector's public error contract.
200210
201211
- ``KernelError`` → mapped class with structured attrs forwarded.
202-
A local missing transfer file maps to ``FileNotFoundError`` without
203-
the OS ``errno`` or ``filename`` attributes.
212+
A local missing transfer file maps to ``FileNotFoundError`` with
213+
its original OS error code when available, without ``filename``.
204214
- Already-mapped PEP 249 or local transfer errors → passed through
205215
unchanged so nested callers preserve their class and attributes.
206216
- Anything else (``TypeError`` / ``ValueError`` / etc. from PyO3

‎src/databricks/sql/backend/kernel/client.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,6 @@ def execute_command(
575575
if staging_verb == "PUT" and re.match(
576576
r"""(['"])__input_stream__\1""",
577577
_strip_leading_sql_comments(stripped_sql[3:]),
578-
re.IGNORECASE,
579578
):
580579
raise NotSupportedError(
581580
"Streaming PUT is not supported by the kernel backend."

‎tests/unit/test_kernel_client.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
from __future__ import annotations
1515

16+
import errno
17+
import os
1618
import sys
1719
import types
1820
from pathlib import Path
@@ -820,6 +822,7 @@ def test_execute_command_forwards_row_limit(row_limit):
820822
"operation, uses_paths",
821823
[
822824
("PUT '/tmp/files/input' INTO '/Volumes/c/s/v/input'", True),
825+
("PUT '__INPUT_STREAM__' INTO '/Volumes/c/s/v/input'", True),
823826
("GET '/Volumes/c/s/v/input' TO '/tmp/files/output'", True),
824827
("/* download */ GET '/Volumes/c/s/v/input' TO '/tmp/files/output'", True),
825828
(
@@ -948,6 +951,7 @@ def test_staging_verb_matches_only_the_leading_statement(operation, expected):
948951
"query",
949952
[
950953
"PUT '__input_stream__' INTO '/Volumes/c/s/v/file'",
954+
"put '__input_stream__' INTO '/Volumes/c/s/v/file'",
951955
"PUT '__input_stream__'INTO '/Volumes/c/s/v/file'",
952956
'PUT"__input_stream__"INTO "/Volumes/c/s/v/file"',
953957
"/* upload */ PUT '__input_stream__' INTO '/Volumes/c/s/v/file'",
@@ -2750,6 +2754,43 @@ def test_file_transfer_error_matches_thrift_exception(
27502754
assert result.error_code == error_code
27512755
assert result.code == code
27522756
assert result.http_status == http_status
2757+
if issubclass(expected, OSError):
2758+
assert result.errno is None
2759+
2760+
2761+
@pytest.mark.parametrize(
2762+
"code,error_code,raw,expected",
2763+
[
2764+
("NotFound", "FILE_TRANSFER_ERROR", errno.ENOENT, FileNotFoundError),
2765+
(
2766+
"PermissionDenied",
2767+
"FILE_TRANSFER_PERMISSION_ERROR",
2768+
errno.EACCES,
2769+
PermissionError,
2770+
),
2771+
(
2772+
"PermissionDenied",
2773+
"FILE_TRANSFER_PERMISSION_ERROR",
2774+
errno.EPERM,
2775+
PermissionError,
2776+
),
2777+
],
2778+
)
2779+
def test_local_transfer_error_preserves_os_code(code, error_code, raw, expected):
2780+
error = _FakeKernelError(code=code, message="File transfer failed")
2781+
error.error_code = error_code
2782+
if os.name == "nt":
2783+
raw = 2 if expected is FileNotFoundError else 5
2784+
error.os_error_code = raw
2785+
result = kernel_client._reraise_kernel_error(error)
2786+
assert type(result) is expected
2787+
assert result.os_error_code == raw
2788+
assert result.filename is None
2789+
if os.name == "nt":
2790+
assert result.winerror == raw
2791+
assert result.errno == (errno.ENOENT if raw == 2 else errno.EACCES)
2792+
else:
2793+
assert result.errno == raw
27532794

27542795

27552796
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)