Skip to content

Commit 83fc674

Browse files
fix(volume): preserve filesystem errors and unify transfer detection
Signed-off-by: Aakash Saravanan <aakash.saravanan@databricks.com>
1 parent 865f6a6 commit 83fc674

3 files changed

Lines changed: 25 additions & 34 deletions

File tree

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,20 +120,23 @@
120120

121121
def reraise_kernel_error(
122122
exc: "_kernel.KernelError",
123-
) -> "Union[Error, FileNotFoundError]":
123+
) -> "Union[Error, OSError]":
124124
"""Map a kernel error and forward its structured attributes.
125125
126-
Local ``FILE_TRANSFER_ERROR`` errors with ``NotFound`` and no HTTP
127-
status use the built-in ``FileNotFoundError``; other errors use
128-
PEP 249 exception classes.
126+
Local missing files use ``FileNotFoundError``; filesystem permission
127+
failures use ``PermissionError``. Other errors use PEP 249 classes.
129128
130129
The returned exception is raised by callers with ``raise ... from
131130
exc``; the ``from`` clause is what sets ``__cause__``, so we don't
132131
touch it here.
133132
"""
134133
code = getattr(exc, "code", "Unknown")
135134
cls = _CODE_TO_EXCEPTION.get(code, DatabaseError)
136-
file_transfer = getattr(exc, "error_code", None) == "FILE_TRANSFER_ERROR"
135+
error_code = getattr(exc, "error_code", None)
136+
file_transfer = error_code in (
137+
"FILE_TRANSFER_ERROR",
138+
"FILE_TRANSFER_PERMISSION_ERROR",
139+
)
137140
if file_transfer:
138141
cls = (
139142
ProgrammingError
@@ -157,9 +160,11 @@ def reraise_kernel_error(
157160
"diagnostic-info": getattr(exc, "diagnostic_info", None),
158161
}
159162
message = getattr(exc, "message", str(exc))
160-
new: Union[Error, FileNotFoundError]
163+
new: Union[Error, OSError]
161164
# Local missing files have no HTTP status; remote missing files carry 404.
162-
if (
165+
if error_code == "FILE_TRANSFER_PERMISSION_ERROR":
166+
new = PermissionError(message)
167+
elif (
163168
file_transfer
164169
and code == "NotFound"
165170
and getattr(exc, "http_status", None) is None
@@ -187,9 +192,7 @@ def reraise_kernel_error(
187192
return new
188193

189194

190-
def wrap_kernel_exception(
191-
what: str, exc: BaseException
192-
) -> "Union[Error, FileNotFoundError]":
195+
def wrap_kernel_exception(what: str, exc: BaseException) -> "Union[Error, OSError]":
193196
"""Map a PyO3 exception to the connector's public error contract.
194197
195198
- ``KernelError`` → mapped class with structured attrs forwarded.

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

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@
7474
# per-request skip-and-warn.
7575
_KERNEL_MANAGED_HEADERS = frozenset({"authorization", "x-databricks-org-id"})
7676

77-
# Transfer detection controls local-path forwarding and the older-wheel guard.
78-
_STAGING_VERBS = ("PUT", "GET", "REMOVE")
79-
80-
8177
def _strip_leading_sql_comments(sql: str) -> str:
8278
"""Strip leading whitespace and SQL comments (``-- …`` line and
8379
``/* … */`` block, possibly several) from ``sql``, returning the
@@ -143,19 +139,10 @@ def _catalog_or_none(value: Optional[str]) -> Optional[str]:
143139
return value
144140

145141

146-
def _is_staging_statement(operation: str) -> bool:
147-
"""True iff ``operation`` is a volume/staging statement (PUT / GET /
148-
REMOVE).
149-
150-
Strips leading whitespace + SQL comments first (so a comment-
151-
prefixed staging op is still caught), then matches the leading token
152-
only — so a normal query that merely *contains* the word (e.g.
153-
``SELECT 'GET' AS x``) isn't misflagged.
154-
"""
155-
stripped = _strip_leading_sql_comments(operation)
156-
# First whitespace-delimited token, uppercased.
157-
verb = stripped.split(None, 1)[0].upper() if stripped.strip() else ""
158-
return verb in _STAGING_VERBS
142+
def _staging_verb(stripped_sql: str) -> Optional[str]:
143+
"""Match a leading verb so SELECT 'GET' cannot enable local file access."""
144+
match = re.match(r"(PUT|GET|REMOVE)\b", stripped_sql, re.IGNORECASE)
145+
return match.group(1).upper() if match else None
159146

160147

161148
def _kernel_session_accepts_kwarg(name: str) -> bool:
@@ -576,9 +563,11 @@ def execute_command(
576563
close_stmt = True
577564
try:
578565
try:
566+
stripped_sql = _strip_leading_sql_comments(operation)
567+
staging_verb = _staging_verb(stripped_sql)
579568
if re.match(
580569
r"""PUT\s+(['"])__input_stream__\1(?:\s|$)""",
581-
_strip_leading_sql_comments(operation),
570+
stripped_sql,
582571
re.IGNORECASE,
583572
):
584573
raise NotSupportedError(
@@ -587,11 +576,7 @@ def execute_command(
587576
stmt.set_sql(operation)
588577
allowed_paths = (
589578
cursor.connection.staging_allowed_local_path
590-
if re.match(
591-
r"(?:PUT|GET)\b",
592-
_strip_leading_sql_comments(operation),
593-
re.IGNORECASE,
594-
)
579+
if staging_verb in ("PUT", "GET")
595580
else None
596581
)
597582
set_paths = getattr(stmt, "set_staging_allowed_local_paths", None)
@@ -613,7 +598,7 @@ def execute_command(
613598
"staging_allowed_local_path must be a string or a list of paths"
614599
) from exc
615600
set_paths(normalized_paths)
616-
elif _is_staging_statement(operation):
601+
elif staging_verb is not None:
617602
raise NotSupportedError(
618603
"Volume transfers require a databricks-sql-kernel wheel "
619604
"with staging_allowed_local_paths support."

‎tests/unit/test_kernel_client.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,8 @@ def test_kernel_rejects_streaming_put(query):
926926
"PUT 'a' INTO 'b'",
927927
"-- comment\nGET 'a' TO 'b'",
928928
"/* comment */ REMOVE 'a'",
929+
"GET'/Volumes/c/s/v/file' TO '/tmp/file'",
930+
"/* comment */ REMOVE'/Volumes/c/s/v/file'",
929931
],
930932
)
931933
def test_execute_command_with_older_kernel(operation):
@@ -2679,6 +2681,7 @@ def test_only_reserved_headers_omits_kwarg(self, monkeypatch):
26792681
("PermissionDenied", 403, "FILE_TRANSFER_ERROR", OperationalError),
26802682
("NotFound", None, "FILE_TRANSFER_ERROR", FileNotFoundError),
26812683
("NotFound", 404, "FILE_TRANSFER_ERROR", OperationalError),
2684+
("PermissionDenied", None, "FILE_TRANSFER_PERMISSION_ERROR", PermissionError),
26822685
("NetworkError", None, "FILE_TRANSFER_ERROR", OperationalError),
26832686
("Internal", None, "FILE_TRANSFER_ERROR", OperationalError),
26842687
("Internal", None, None, DatabaseError),

0 commit comments

Comments
 (0)