Skip to content

Commit 9540245

Browse files
fix(kernel): preserve ordinary queries with older native wheels
Signed-off-by: Aakash Saravanan <aakash.saravanan@databricks.com>
1 parent 2cd0562 commit 9540245

6 files changed

Lines changed: 76 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Release History
22

33
# Unreleased
4+
- Support SQL PUT, GET, and REMOVE file transfers with a native kernel exposing `Statement.set_staging_allowed_local_paths`; older wheels reject transfers and continue to support ordinary queries.
45
- Fix inline parameters (`use_inline_params=True`) so a sequence directly after `IN` renders as a list again: `x IN %(ids)s` renders as `x IN (1,2,3)` instead of `x IN ARRAY(1,2,3)`, which Spark rejects. Inside the list, tuples render as rows for a multi-column `IN`, and a `set` or `frozenset` is accepted again. Sequences anywhere else still render as `ARRAY(...)` ([#629](https://github.com/databricks/databricks-sql-python/issues/629))
56

67
# 4.9.0 (2026-11-04)

‎CONNECTION_PARAMETERS.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ to change without notice.
171171
| `use_inline_params` | `bool` \| `"silent"` | ✅ | ✅ | `False` | Render parameters inline (legacy) vs. native bound params (DBR 14.1+). Shared cursor code performs the rendering before dispatch, so both backends honor it identically. |
172172
| `ignore_transactions` | `bool` | ✅ | ✅ | `True` | When `True`: `commit()` is a no-op, `rollback()` raises `NotSupportedError`, and setting `autocommit` is a no-op. |
173173
| `fetch_autocommit_from_server`| `bool` | ✅ | ✅ | `False` | Query the server (`SET AUTOCOMMIT`) for autocommit state instead of returning the cached value. |
174-
| `staging_allowed_local_path` | `str` \| `List[str]` | ✅ | ❌ | `None` | Local path(s) permitted for Unity Catalog Volume `PUT`/`GET`. **Thrift-only** — the kernel has no Volume API yet. |
174+
| `staging_allowed_local_path` | `str` \| `List[str]` | ✅ | ✅ | `None` | Local path(s) permitted for Unity Catalog Volume `PUT`/`GET`. Kernel transfers require a wheel exposing `Statement.set_staging_allowed_local_paths` (kernel [#439](https://github.com/databricks/databricks-sql-kernel/pull/439)); older wheels reject transfer commands. |
175175

176176
## Telemetry
177177

@@ -206,7 +206,6 @@ installed kernel binding. `force_enable_telemetry` remains Python-driver-only.
206206
`enable_query_result_lz4_compression`.
207207
5. Arrow-native rendering for `_use_arrow_native_decimals` /
208208
`_use_arrow_native_timestamps` (complex types **are** forwarded).
209-
6. `staging_allowed_local_path` (Volume `PUT`/`GET`).
210209
7. Retry fine-tuning beyond the four forwarded knobs: `_retry_delay_default`,
211210
`_retry_dangerous_codes`, `_respect_server_retry_after_header`,
212211
`_retry_max_redirects`, `_enable_v3_retries`.

‎KERNEL_REV‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d41cc3657748966d8b69a802bc40e8d019022ceb
1+
bc39c7921acecd95e4eea0740becb883867a834b

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def reraise_kernel_error(
148148
"diagnostic-info": getattr(exc, "diagnostic_info", None),
149149
}
150150
message = getattr(exc, "message", str(exc))
151+
# Local missing files have no HTTP status; remote missing files carry 404.
151152
if (
152153
file_transfer
153154
and code == "NotFound"

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,30 @@
7373
_KERNEL_MANAGED_HEADERS = frozenset({"authorization", "x-databricks-org-id"})
7474

7575

76+
def _strip_leading_sql_comments(sql: str) -> str:
77+
"""Skip SQL comments so unsupported transfers cannot appear successful."""
78+
i = 0
79+
n = len(sql)
80+
while i < n:
81+
if sql[i].isspace():
82+
i += 1
83+
elif sql.startswith("--", i):
84+
nl = sql.find("\n", i)
85+
i = n if nl == -1 else nl + 1
86+
elif sql.startswith("/*", i):
87+
close = sql.find("*/", i + 2)
88+
i = n if close == -1 else close + 2
89+
else:
90+
break
91+
return sql[i:]
92+
93+
94+
def _is_staging_statement(operation: str) -> bool:
95+
stripped = _strip_leading_sql_comments(operation)
96+
verb = stripped.split(None, 1)[0].upper() if stripped.strip() else ""
97+
return verb in ("PUT", "GET", "REMOVE")
98+
99+
76100
def _is_not_found(exc: BaseException) -> bool:
77101
"""True iff ``exc`` is a kernel ``NotFound`` error (HTTP 404 /
78102
``STATEMENT_NOT_FOUND``).
@@ -530,7 +554,14 @@ def execute_command(
530554
allowed_paths = cursor.connection.staging_allowed_local_path
531555
if isinstance(allowed_paths, str):
532556
allowed_paths = [allowed_paths]
533-
stmt.set_staging_allowed_local_paths(allowed_paths or [])
557+
set_paths = getattr(stmt, "set_staging_allowed_local_paths", None)
558+
if callable(set_paths):
559+
set_paths(allowed_paths or [])
560+
elif _is_staging_statement(operation):
561+
raise NotSupportedError(
562+
"Volume transfers require a databricks-sql-kernel wheel "
563+
"with staging_allowed_local_paths support."
564+
)
534565
stmt.set_row_limit(row_limit)
535566
if query_tags:
536567
# Per-statement query tags. The kernel serialises the

‎tests/unit/test_kernel_client.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,46 @@ def test_execute_command_forwards_allowed_local_paths(allowed, expected, async_o
844844
(stmt.submit if async_op else stmt.execute).assert_called_once()
845845

846846

847+
@pytest.mark.parametrize(
848+
"operation",
849+
[
850+
"SELECT 1",
851+
"PUT 'a' INTO 'b'",
852+
"-- comment\nGET 'a' TO 'b'",
853+
"/* comment */ REMOVE 'a'",
854+
],
855+
)
856+
def test_execute_command_with_older_kernel(operation):
857+
c = _make_client()
858+
c._kernel_session = MagicMock()
859+
stmt = c._kernel_session.statement.return_value
860+
del stmt.set_staging_allowed_local_paths
861+
stmt.execute.return_value = MagicMock(
862+
statement_id="00000000-0000-0000-0000-000000000001",
863+
arrow_schema=MagicMock(return_value=pa.schema([("x", pa.int64())])),
864+
)
865+
kwargs = dict(
866+
operation=operation,
867+
session_id=MagicMock(),
868+
max_rows=1,
869+
max_bytes=1,
870+
lz4_compression=False,
871+
cursor=MagicMock(),
872+
use_cloud_fetch=False,
873+
parameters=[],
874+
async_op=False,
875+
enforce_embedded_schema_correctness=False,
876+
)
877+
if operation == "SELECT 1":
878+
c.execute_command(**kwargs)
879+
stmt.execute.assert_called_once()
880+
else:
881+
with pytest.raises(NotSupportedError, match="Volume transfers require"):
882+
c.execute_command(**kwargs)
883+
stmt.execute.assert_not_called()
884+
stmt.close.assert_called_once()
885+
886+
847887
# ---------------------------------------------------------------------------
848888
# Sync cancel wiring (cursor.cancel() during a blocking execute())
849889
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)