Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Release History

# Unreleased
- Fix the kernel backend ignoring `use_cloud_fetch=False`, so complete results can be fetched without cloud-storage downloads.
- 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))

# 4.9.0 (2026-11-04)
Expand Down
4 changes: 2 additions & 2 deletions CONNECTION_PARAMETERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ to change without notice.

| Option | Type | Thrift | Kernel | Default Value | Note |
| ------------------------------------- | ------ | :----: | :----: | ------------- | ------------------------------------------------------------------------------------------------------------- |
| `use_cloud_fetch` | `bool` | ✅ | ❌ | `True` | Download large result sets in parallel from cloud storage. The kernel manages result transport internally. |
| `use_cloud_fetch` | `bool` | ✅ | ✅ | `True` | Set `False` at connection creation to fetch complete results without cloud-storage downloads. On the kernel path, changing this attribute after connecting does not change result delivery. |
| `max_download_threads` | `int` | ✅ | ❌ | `10` | Worker threads for cloud-fetch downloads. Not forwarded to the kernel. |
| `enable_query_result_lz4_compression` | `bool` | ✅ | ❌ | `True` | LZ4-compress result payloads. Not forwarded; the kernel handles compression internally. |
| `_disable_pandas` | `bool` | ✅ | ✅ | `False` | Skip the pandas-based Arrow→row deserialization and materialize rows directly with PyArrow. This is a **Python-side** result-conversion toggle, not a wire option: the kernel returns results as Arrow (`RecordBatch`es) and the connector runs the *same* `_convert_arrow_table` for both backends, so the flag is honored on the kernel path too. Affects only row fetches (`fetchone`/`fetchmany`/`fetchall`); the `fetch*_arrow` methods return the Arrow table unchanged regardless of this flag. |
Expand Down Expand Up @@ -202,7 +202,7 @@ installed kernel binding. `force_enable_telemetry` remains Python-driver-only.
2. `experimental_oauth_persistence` (custom OAuth token store).
3. TLS-client-cert *authentication* (`_use_cert_as_auth`) — note the TLS
*transport* options (`_tls_*`) themselves **are** honored on both backends.
4. Result-transport tuning: `use_cloud_fetch`, `max_download_threads`,
4. Result-transport tuning: `max_download_threads`,
`enable_query_result_lz4_compression`.
5. Arrow-native rendering for `_use_arrow_native_decimals` /
`_use_arrow_native_timestamps` (complex types **are** forwarded).
Expand Down
2 changes: 1 addition & 1 deletion KERNEL_REV
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6f4b6d607a61bdde61dcda2cf82bc8e80fd32cb9
0b5d0edfed32c0745d5abc2468fc3070e2eb2a17
8 changes: 5 additions & 3 deletions src/databricks/sql/backend/kernel/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ def __init__(
# The kernel binding owns type and range validation.
self._request_timeout_secs = kwargs.get("request_timeout_secs")
self._max_connections = kwargs.get("max_connections")
self._use_cloud_fetch = kwargs.get("use_cloud_fetch", True)
Comment thread
aakash-saravanan-db marked this conversation as resolved.
Resolved
# Opt-in native GEOMETRY / GEOGRAPHY support. True requests
# the canonical Arrow struct and surfaces as ``{"srid": int, "wkb":
# bytes}`` through pyarrow; False requests WKT / EWKT strings. This is
Expand Down Expand Up @@ -390,12 +391,13 @@ def open_session(
) -> SessionId:
if self._kernel_session is not None:
raise InterfaceError("KernelDatabricksClient already has an open session.")
# ``session_configuration`` flows through to the kernel's
# ``session_conf`` map verbatim; the SEA endpoint enforces
# its own allow-list and rejects unknown keys.
# The kernel consumes client settings locally before forwarding server parameters.
session_conf: Optional[Dict[str, str]] = None
if session_configuration:
session_conf = {k: str(v) for k, v in session_configuration.items()}
if not self._use_cloud_fetch:
Comment thread
aakash-saravanan-db marked this conversation as resolved.
Resolved
Comment thread
peco-review-bot[bot] marked this conversation as resolved.
Resolved

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High — This branch now injects cloudfetch_enabled="false" into session_conf, but the declared floor is still ^1.4.0, and the PR says released 1.4.0 doesn't consume this key locally. On a 1.4.0 wheel the key goes straight to the server as a session parameter. The comment this PR removes said the SEA endpoint "enforces its own allow-list and rejects unknown keys". So use_kernel=True, use_cloud_fetch=False used to connect (cloud fetch was just ignored) and may now fail at open_session for anyone the resolver puts on 1.4.0.

The PR description already calls this a merge gate. Please don't merge until the floor in pyproject.toml/poetry.lock is bumped to the first native release containing kernel#436. If that can't land together with this change, gate the injection on a capability check, as the other kernel options do.

(Anchored to the nearest changed line — see the description for the exact location.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged; this is the same native-release gate tracked in the current dependency thread. This PR must remain unmerged until the native release containing kernel #436 is published and both pyproject.toml and poetry.lock require it. The release version is not assigned yet. Leaving this thread open until those dependency updates are present.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for confirming. The code at line 398 still adds cloudfetch_enabled="false" without any check, and the declared floor is still ^1.4.0, so the 1.4.0 regression is still possible. Leaving this thread open as the merge gate. It's resolved once pyproject.toml and poetry.lock require the native release that contains kernel#436. If that release slips, a capability check around the injection would also resolve it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High — This branch now sends cloudfetch_enabled="false" whenever a caller passes use_cloud_fetch=False. The PR says the kernel only delivers large results correctly with CloudFetch disabled after databricks-sql-kernel#436. But pyproject.toml:73 still allows ^1.4.0, and only KERNEL_REV (used for source builds) points at the fix.

On any wheel in the allowed range, users who already set use_cloud_fetch=False (it was silently ignored before, so results came back fine via CloudFetch) would now get the broken disabled-CloudFetch behavior on large results. On a wheel that doesn't consume the key locally, it would instead be forwarded to the server as an unknown session conf. Either way, existing working setups regress.

The PR description already lists the pin bump as a pre-merge step. Please make it a hard gate: bump the floor in pyproject.toml / poetry.lock in this PR, or hold the merge, so the resolver can't pick a kernel without the fix.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: this PR remains merge-gated until the native package containing kernel #436 is published and both pyproject.toml and poetry.lock require that release. The release version is still unassigned. The PR description retains this prerequisite, and the documentation cleanup leaves that gate in place.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for confirming the merge gate. The code hasn't changed yet, though: pyproject.toml and poetry.lock still allow kernel wheels without #436, so the regression risk is still there. I'll leave this thread open as the gate. It can be resolved once the PR bumps the floor in both files to the release that includes #436.

session_conf = session_conf or {}
Comment thread
peco-review-bot[bot] marked this conversation as resolved.
Resolved

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — cloudfetch_enabled only works as a client-side setting in a kernel build that includes databricks-sql-kernel#436. pyproject.toml:73 still allows ^1.4.0. Per the comment this PR deleted, a 1.4.0 binary forwards session_conf unchanged to the SEA endpoint, which rejects unknown keys. So on the wheel that resolves today, use_cloud_fetch=False would go from "ignored" to either failing at open_session or being silently ignored by the server. The PR description already lists this as a merge gate. Please make sure this PR includes the floor bump (and the poetry.lock refresh) before merge, so the gate lives in dependency metadata and not only in CONNECTION_PARAMETERS.md prose.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged; this is the same native-release gate tracked in the current dependency thread. This PR must remain unmerged until the native release containing kernel #436 is published and both pyproject.toml and poetry.lock require it. The release version is not assigned yet. Leaving this thread open until those dependency updates are present.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for confirming. Your plan to keep this unmerged until the native release with kernel #436 ships, and until both pyproject.toml and poetry.lock require that version, is the right gate. No dependency bump is visible yet: pyproject.toml still allows ^1.4.0, and the code at line 399 still sends cloudfetch_enabled unconditionally when use_cloud_fetch=False. So this thread stays open as the blocking item until the floor bump and the lock refresh land.

session_conf["cloudfetch_enabled"] = "false"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — This only works if the installed kernel reads cloudfetch_enabled as a client-side setting and doesn't send it to the server. That behavior comes from databricks-sql-kernel#436, which no released wheel includes yet. pyproject.toml still allows ^1.4.0, and KERNEL_REV only pins the source used for validation.

On a 1.4.x wheel, the key goes into session_conf and is sent to the server unchanged. The comment this PR removes said the SEA endpoint "enforces its own allow-list and rejects unknown keys". If that's right, every use_kernel=True, use_cloud_fetch=False connect fails at session open. If the server ignores the key instead, results still come back via cloud storage, which is the bug this PR is fixing. In both cases the user has no way to see the setting was dropped.

Please bump the databricks-sql-kernel floor in pyproject.toml to the first release that includes #436, in the same change (or as a release-gated follow-up). Until then, don't mark the CONNECTION_PARAMETERS.md row as ✅ without noting the minimum kernel version.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This connector is released together with an updated native kernel; compatibility with the released 1.4.0 binary is not required. The documentation now explicitly requires the native release containing kernel #436 and a coordinated published-dependency update. No future version is assigned yet. KERNEL_REV pins the changed source, and validation uses a locally rebuilt native wheel from that revision.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs half of this is done: CONNECTION_PARAMETERS.md now names the #436 kernel requirement. The packaging half isn't. pyproject.toml still declares databricks-sql-kernel = "^1.4.0", so anyone installing the [kernel] extra from this tree can get a 1.4.x wheel and hit the silent failure from the original finding. A coordinated release plan is a fine reason to hold off on picking a version number now. But nothing in the repo enforces that plan, and a requirement that only lives in the docs is how PR #1001 regressed. I'll leave this thread open as the release-gate reminder: the floor needs to go up to the first release that includes #436 before or alongside this connector release. A tracking issue linked here would also work.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High — This adds cloudfetch_enabled="false" to session_conf whenever use_cloud_fetch=False. Only a kernel with databricks-sql-kernel#436 reads that key locally. The dependency floor is still ^1.4.0 (pyproject.toml:73), and your own docs say 1.4.0 "does not provide this behavior."

On 1.4.0, session_conf goes to the server as-is. The comment this PR replaces said: "the SEA endpoint enforces its own allow-list and rejects unknown keys." So a user who already passed use_cloud_fetch=False with use_kernel=True would go from having the option silently ignored to having open_session fail. If the server ignores the key instead, the option still does nothing and nobody is told.

Possible fixes:

  • Bump the kernel floor in pyproject.toml in this PR, or gate merging on that bump.
  • Or only send the key when the installed wheel supports it, the same way _kernel_session_accepts_kwarg gates other options. If it isn't supported, raise a clear NotSupportedError, but only when the user explicitly passed False.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an explicit pre-merge gate to the PR description: the native release containing kernel #436 must be published and the minimum dependency updated in this PR before it can merge. No compatibility fallback or guessed future version is introduced. The current changes are validated from the pinned native source. The option documentation also explicitly requires a Python bool; accepting string values would change the existing Thrift input contract.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, gating the merge on the kernel release is one of the fixes I suggested, so I'm fine with that approach. But the gate is only written in the PR description so far. pyproject.toml:73 still says databricks-sql-kernel = "^1.4.0", so if this merged today, a 1.4.0 install would still send cloudfetch_enabled to the server. I'm keeping this thread open so it stays in the unresolved queue and blocks the merge. It can be closed once the floor is raised to the first release that includes kernel #436. I agree with leaving out a compatibility fallback, and with keeping the option bool-only to match Thrift.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — The fix only works with databricks-sql-kernel #436 (large-result delivery with CloudFetch off), but only KERNEL_REV is bumped. pyproject.toml / poetry.lock still allow older wheels. If the resolver picks one of those, use_cloud_fetch=False now sends cloudfetch_enabled=false to a kernel that can't deliver large results without CloudFetch. Large results would then break or come back incomplete, even though the CHANGELOG and CONNECTION_PARAMETERS.md now list the option as supported (✅). Please make the floor bump part of this PR, or block the merge on it, so the docs and the dependency graph match. Otherwise, document that the option needs kernel ≥ the release containing #436.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged; this is the same native-release gate tracked in the current dependency thread. This PR must remain unmerged until the native release containing kernel #436 is published and both pyproject.toml and poetry.lock require it. The release version is not assigned yet. Leaving this thread open until those dependency updates are present.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for confirming. Agreed: the right fix is to keep this PR unmerged until the native release containing kernel #436 ships and both pyproject.toml and poetry.lock require that version. Right now the dependency graph still allows older wheels, and the docs and CHANGELOG already mark use_cloud_fetch=False as supported. I'm leaving this thread open until the floor bump is in the diff so it stays in the unresolved queue as a merge blocker.

# The kwarg builds run INSIDE the try so the ``finally`` scrub
# below always fires — including when ``kernel_auth_kwargs``
# itself raises mid-build (e.g. an OAuth token-exchange failure
Expand Down
1 change: 1 addition & 0 deletions src/databricks/sql/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ def _create_backend(
retry_options=kernel_retry_options,
request_timeout_secs=kwargs.get("_socket_timeout"),
max_connections=kwargs.get("_pool_maxsize") or None,
use_cloud_fetch=kwargs.get("use_cloud_fetch", True),
Comment thread
aakash-saravanan-db marked this conversation as resolved.
Resolved
enable_geospatial_support=kwargs.get(
"enable_geospatial_support", False
),
Expand Down
86 changes: 86 additions & 0 deletions tests/e2e/test_kernel_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@

from __future__ import annotations

import hashlib
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import select
import socket
import sys
import threading
from uuid import uuid4

import pytest
Expand Down Expand Up @@ -136,6 +141,87 @@ def conn(kernel_conn_params):
c.close()


@pytest.fixture
def workspace_only_proxy(kernel_conn_params, monkeypatch):
connections = {"workspace": 0, "blocked": 0}
workspace = kernel_conn_params["server_hostname"] + ":443"
stopping = threading.Event()

class Handler(BaseHTTPRequestHandler):
def do_CONNECT(self):
self.close_connection = True
if self.path != workspace:
connections["blocked"] += 1
self.send_error(403)
return
with socket.create_connection(
(kernel_conn_params["server_hostname"], 443), timeout=30
) as upstream:
connections["workspace"] += 1
self.send_response(200)
self.end_headers()
while not stopping.is_set():
ready, _, _ = select.select([self.connection, upstream], [], [], 1)
for source in ready:
data = source.recv(65536)
if not data:
return
target = (
upstream if source is self.connection else self.connection
)
target.sendall(data)

def log_message(self, *_args):
pass

proxy = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=proxy.serve_forever, daemon=True)
thread.start()
for name in ("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"):
monkeypatch.setenv(name, f"http://127.0.0.1:{proxy.server_port}")
for name in ("NO_PROXY", "no_proxy"):
monkeypatch.setenv(name, "")
try:
yield connections
finally:
stopping.set()
proxy.shutdown()
proxy.server_close()
thread.join()


def test_cloudfetch_disabled_returns_complete_large_result(
kernel_conn_params, workspace_only_proxy
):
from databricks.sql.backend.kernel.client import KernelDatabricksClient

with sql.connect(**kernel_conn_params, use_cloud_fetch=False) as connection:
assert isinstance(connection.session.backend, KernelDatabricksClient)
with connection.cursor() as cursor:
cursor.execute(
"SELECT id, sha2(CAST(id AS STRING), 512) AS payload "
"FROM range(250000) ORDER BY id"
)
count = 0
payload_bytes = 0
digest = hashlib.sha256()
while rows := cursor.fetchmany(8192):
for identity, payload in rows:
assert identity == count
assert payload == hashlib.sha512(str(identity).encode()).hexdigest()
payload_bytes += len(payload)
digest.update(f"{identity}\t{payload}\n".encode())
count += 1
assert count == 250000
assert payload_bytes == 32000000
assert digest.hexdigest() == (
"a25ada4c6c476b53e97eb6e8a945e9ce4c14cc432c7ad672dc6fb607d660c86a"
)

assert workspace_only_proxy["workspace"] > 0
assert workspace_only_proxy["blocked"] == 0


Comment thread
aakash-saravanan-db marked this conversation as resolved.
Resolved
def test_connect_with_use_kernel_opens_a_session(conn):
assert conn.open, "connection should report open after connect()"

Expand Down
52 changes: 52 additions & 0 deletions tests/unit/test_kernel_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,58 @@ def fake_session(**kw):
assert captured.get("complex_types_as_json") is expected_flag


@pytest.mark.parametrize(
"kwargs, configuration, expected",
[
({}, {"TIMEZONE": "UTC"}, {"TIMEZONE": "UTC"}),
(
{"use_cloud_fetch": True},
{"can_cloud_download": "false"},
{"can_cloud_download": "false"},
),
({"use_cloud_fetch": False}, None, {"cloudfetch_enabled": "false"}),
(
{"use_cloud_fetch": False},
{"TIMEZONE": "UTC"},
{"TIMEZONE": "UTC", "cloudfetch_enabled": "false"},
),
(
{"use_cloud_fetch": False},
{"can_cloud_download": "true"},
{"can_cloud_download": "true", "cloudfetch_enabled": "false"},
),
(
{"use_cloud_fetch": False},
{"CLOUDFETCH_ENABLED": "true", "cloudfetch_enabled": "true"},
Comment thread
peco-review-bot[bot] marked this conversation as resolved.
Resolved
{"CLOUDFETCH_ENABLED": "true", "cloudfetch_enabled": "false"},
Comment thread
aakash-saravanan-db marked this conversation as resolved.
Resolved
),
],
)
def test_open_session_honors_cloudfetch_option(
monkeypatch, kwargs, configuration, expected
):
captured = {}

def fake_session(**options):
captured.update(options)
session = MagicMock()
session.session_id = "sess-id"
return session

monkeypatch.setattr(kernel_client._kernel, "Session", fake_session)
client = kernel_client.KernelDatabricksClient(
server_hostname="example.cloud.databricks.com",
http_path="/sql/1.0/warehouses/abc",
auth_provider=AccessTokenAuthProvider("dapi-test"),
ssl_options=None,
**kwargs,
)
original = dict(configuration) if configuration is not None else None
client.open_session(configuration, catalog=None, schema=None)
assert captured["session_conf"] == expected
assert configuration == original


@pytest.mark.parametrize("timeout", [None, 0, 12.5])
def test_open_session_passes_request_timeout_to_kernel(monkeypatch, timeout):
captured = {}
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ def test_retry_and_socket_timeout_threaded_into_kernel_client(self):
_retry_stop_after_attempts_duration=600.0,
_socket_timeout=12.5,
_pool_maxsize=41,
use_cloud_fetch=False,
enable_geospatial_support=False,
)
try:
Expand All @@ -549,6 +550,7 @@ def test_retry_and_socket_timeout_threaded_into_kernel_client(self):
assert opts["retry_stop_after_attempts_duration"] == 600.0
assert kwargs["request_timeout_secs"] == 12.5
assert kwargs["max_connections"] == 41
assert kwargs["use_cloud_fetch"] is False
assert kwargs["enable_geospatial_support"] is False
finally:
conn.close()
Expand Down
Loading