Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -4,6 +4,7 @@
- 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))
- Speed up Thrift response handling: read replies in 1 MiB chunks and decode them with Thrift's C-accelerated protocol, and stop stringifying every request/response for the default no-op debug logger. Wide result schemas, metadata calls with many rows, and inline Arrow results benefit most.
- Speed up the first `connect()` in a process by importing pandas only when rows are first converted through it.

# 4.9.0 (2026-11-04)
- Upgrade Databricks SQL Kernel to 1.4.0, which keeps Lakehouse Real-Time session affinity and flushes shared telemetry only after the final session closes.
Expand Down
1 change: 0 additions & 1 deletion src/databricks/sql/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
BinaryIO,
TYPE_CHECKING,
)
import pandas

try:
import pyarrow
Expand Down
5 changes: 4 additions & 1 deletion src/databricks/sql/result_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from typing import List, Optional, TYPE_CHECKING, Tuple

import logging
import pandas

try:
import pyarrow
Expand Down Expand Up @@ -103,6 +102,10 @@ def _convert_arrow_table(self, table):
ResultRow(*[v.as_py() for v in r]) for r in zip(*table.itercolumns())
]

# Imported on first use: pandas takes ~165 ms to import, and connect()
# used to pay that under the import lock for every thread connecting.
import pandas

# Need to use nullable types, as otherwise type can change when there are missing values.
# See https://arrow.apache.org/docs/python/pandas.html#nullable-types
# NOTE: This api is epxerimental https://pandas.pydata.org/pandas-docs/stable/user_guide/integer_na.html
Expand Down
113 changes: 113 additions & 0 deletions tests/unit/test_lazy_pandas_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""pandas must load on the first row conversion, not when connect() is imported.

Each check runs in a fresh interpreter because ``sys.modules`` is process-global.
"""

import subprocess
import sys
import textwrap

import pytest

pytest.importorskip("pyarrow")

CONNECT_PATH_MODULES = [
Comment thread
peco-review-bot[bot] marked this conversation as resolved.
Resolved
Comment thread
peco-review-bot[bot] marked this conversation as resolved.
Resolved
"databricks.sql",
"databricks.sql.client",
"databricks.sql.result_set",
"databricks.sql.utils",
"databricks.sql.backend.thrift_backend",
"databricks.sql.backend.kernel.client",
"databricks.sql.backend.kernel.result_set",
"databricks.sql.backend.sea.backend",
]


def _run(script: str) -> subprocess.CompletedProcess:
return subprocess.run(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low — _run calls subprocess.run with no timeout. If a child interpreter hangs during import (for example, a kernel wheel that blocks at load), the unit job stalls until CI's global timeout, with no useful failure message. Adding a bounded timeout= (e.g. 120 s) would turn a hang into a clear TimeoutExpired failure.

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 timeout=120 to _run in 916638b.

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 picking this up. The head checkout still doesn't have the timeout, though. _run in tests/unit/test_lazy_pandas_import.py calls subprocess.run with only capture_output=True and text=True. There's no timeout=120, so commit 916638b either wasn't pushed to this branch or doesn't include that change. Could you check that the commit is on the PR branch? Once timeout= shows up in _run, this is resolved.

[sys.executable, "-c", textwrap.dedent(script)],
capture_output=True,
text=True,
timeout=120,
)


def _table_ipc_hex() -> str:
# Children read the table from IPC bytes: building it from Python lists
# makes pyarrow import pandas itself, which would hide what the
# conversion imports.
import io

import pyarrow
import pyarrow.ipc

table = pyarrow.table({"a": [1, None], "b": ["x", "y"]})
sink = io.BytesIO()
with pyarrow.ipc.new_stream(sink, table.schema) as writer:
writer.write_table(table)
return sink.getvalue().hex()


def _convert_rows_script(disable_pandas: bool) -> str:
return f"""
import sys
from unittest.mock import MagicMock
import pyarrow.ipc
from databricks.sql.result_set import ResultSet

table = pyarrow.ipc.open_stream(bytes.fromhex("{_table_ipc_hex()}")).read_all()
assert "pandas" not in sys.modules
rs = MagicMock()
rs.connection.disable_pandas = {disable_pandas}
rs.description = [("a",), ("b",)]
rows = ResultSet._convert_arrow_table(rs, table)
assert [tuple(r) for r in rows] == [(1, "x"), (None, "y")], rows
print("pandas" in sys.modules)
"""


# Sentinel exit codes, distinct from the 1 of an uncaught exception.
_NO_PANDAS, _PANDAS_LOADED, _MISSING_OPTIONAL_DEP = 10, 11, 12
# Only a missing optional extra may skip; any other import error fails.
_OPTIONAL_DEPS = ("databricks_sql_kernel", "pyarrow")


@pytest.mark.parametrize("module_name", CONNECT_PATH_MODULES)
def test_connect_path_does_not_import_pandas(module_name):
proc = _run(f"""
import sys
try:
import {module_name} # noqa: F401
except ImportError as e:
# The kernel backend re-raises a missing wheel as a friendlier
# ImportError, so look for the missing extra along the chain.
cause = e
while cause is not None and not (
isinstance(cause, ModuleNotFoundError)
and cause.name in {_OPTIONAL_DEPS!r}
):
cause = cause.__cause__ or cause.__context__
if cause is None:
raise
print(cause.name)
sys.exit({_MISSING_OPTIONAL_DEP})
sys.exit({_PANDAS_LOADED} if "pandas" in sys.modules else {_NO_PANDAS})
""")
if proc.returncode == _MISSING_OPTIONAL_DEP:
pytest.skip(f"{module_name} needs {proc.stdout.strip()}, not installed")
assert proc.returncode != _PANDAS_LOADED, f"importing {module_name} imported pandas"
assert (
proc.returncode == _NO_PANDAS
), f"unexpected exit {proc.returncode}:\n{proc.stderr}"


def test_row_conversion_imports_pandas_on_first_use():
proc = _run(_convert_rows_script(disable_pandas=False))
assert proc.returncode == 0, proc.stderr
assert proc.stdout.strip() == "True", "the pandas path should import pandas"


def test_disable_pandas_never_imports_pandas():
proc = _run(_convert_rows_script(disable_pandas=True))
assert proc.returncode == 0, proc.stderr
assert proc.stdout.strip() == "False", "_disable_pandas=True imported pandas"
Loading