Skip to content

Commit 4106045

Browse files
committed
Merge branch 'main' into fix-decimal-precision-scale-901
Preserve the reviewed decimal compatibility entry under Unreleased while keeping the upstream 4.7.0 release notes unchanged. The decimal implementation and regression blobs are unchanged.
2 parents 074e873 + 285a74c commit 4106045

15 files changed

Lines changed: 354 additions & 40 deletions

‎CHANGELOG.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
# Unreleased
44
- Correct explicit `DecimalParameter` precision and scale ordering and honor a declared zero scale. Callers that followed the previously reversed parameter descriptions must use `precision` for total digits and `scale` for fractional digits. Explicit zero precision is now sent as declared instead of silently using automatic sizing.
5-
- Add the `escape_catalog_name` connection parameter (default `False`) to opt in to escaping unescaped `_` in metadata catalog names on Thrift and kernel connections. Existing escapes and `%` wildcards are preserved.
5+
6+
# 4.7.0 (2026-10-20)
7+
- Upgrade Databricks SQL Kernel to 1.2.0.
8+
- Add the kernel-only `enable_geospatial_support` option (default `False`) to return GEOMETRY / GEOGRAPHY values as SRID/WKB objects instead of WKT/EWKT strings.
9+
- Add opt-in `escape_catalog_name` support (default `False`) for metadata queries on Thrift and kernel connections.
10+
- Share feature-flag values across connections to the same workspace, with typed reads and coordinated refreshes.
11+
- Allow Apache Thrift 0.25.x and OAuthlib 4.x, including security fixes while preserving support for large Thrift results.
612

713
# 4.6.0 (2026-09-24)
814
- Upgrade Databricks SQL Kernel to 1.1.0; the kernel dependency is now stable and no longer experimental.

‎CONNECTION_PARAMETERS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ to change without notice.
153153
| `max_download_threads` | `int` | ✅ | ❌ | `10` | Worker threads for cloud-fetch downloads. Not forwarded to the kernel. |
154154
| `enable_query_result_lz4_compression` | `bool` | ✅ | ❌ | `True` | LZ4-compress result payloads. Not forwarded; the kernel handles compression internally. |
155155
| `_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. |
156+
| `enable_geospatial_support` | `bool` | ❌ | ✅ | `False` | Return GEOMETRY / GEOGRAPHY as `{"srid": int, "wkb": bytes}` values when `True`, or as WKT / EWKT strings when `False`. This is a local result conversion and is never forwarded to SEA. |
156157
| `_use_arrow_native_complex_types` | `bool` | ✅ | ✅ | `True` | Return `ARRAY`/`MAP`/`STRUCT` as native Arrow types instead of JSON strings. Forwarded to the kernel. |
157158
| `_use_arrow_native_decimals` | `bool` | ✅ | ❌ | `True` | Thrift wire encoding for `DECIMAL`: `True` → native Arrow `decimal128`, `False` → Arrow string. **No value-level effect**, though: the connector unconditionally re-casts the column back to `decimal128` (`convert_decimals_in_arrow_table`, `thrift_backend.py`), so both `fetchall()` and `fetchall_arrow()` yield `Decimal` / `decimal128(p,s)` either way (verified live). Not forwarded to the kernel, which always returns native Arrow decimals. |
158159
| `_use_arrow_native_timestamps` | `bool` | ✅ | ❌ | `True` | Thrift wire encoding for `TIMESTAMP`: `True` → native Arrow timestamp (→ Python `datetime`), `False` → Arrow string (→ Python **`str`**). **Unlike decimals there is no re-cast**, so `False` genuinely surfaces strings — and `cursor.description` still reports the type code as `'timestamp'`, a mismatch to watch for (verified live). Note the connector always also sends the `spark.thriftserver.arrowBasedRowSet.timestampAsString=false` conf, but the `timestampAsArrow=False` flag wins. Not forwarded to the kernel, which always returns native Arrow timestamps. |

‎KERNEL_REV‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ab1e6a707e4a15a3a7a8c78d8fdd12c752c51102
1+
39ba84f5ab55c2b0bc874fd58a6f9215ee27cb23

‎poetry.lock‎

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "databricks-sql-connector"
3-
version = "4.6.0"
3+
version = "4.7.0"
44
description = "Databricks SQL Connector for Python"
55
authors = ["Databricks <databricks-sql-connector-maintainers@databricks.com>"]
66
license = "Apache-2.0"
@@ -67,10 +67,10 @@ requests-kerberos = {version = "^0.15.0", optional = true}
6767
# is ``abi3`` with ``Requires-Python: >=3.9`` (built ``abi3-py39``); the
6868
# dependency remains gated to the connector's Python >= 3.10 support floor.
6969
#
70-
# Floor is 1.1.0 (``^1.1.0`` == ``>=1.1.0,<2.0.0``), allowing compatible
70+
# Floor is 1.2.0 (``^1.2.0`` == ``>=1.2.0,<2.0.0``), allowing compatible
7171
# 1.x releases while keeping the next major upgrade deliberate. The kernel's
7272
# pyarrow constraint remains >=23.0.1,<24.
73-
databricks-sql-kernel = {version = "^1.1.0", optional = true, python = ">=3.10"}
73+
databricks-sql-kernel = {version = "^1.2.0", optional = true, python = ">=3.10"}
7474

7575

7676
[tool.poetry.extras]

‎src/databricks/sql/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __repr__(self):
7171
DATE = DBAPITypeObject("date")
7272
ROWID = DBAPITypeObject()
7373

74-
__version__ = "4.6.0"
74+
__version__ = "4.7.0"
7575
USER_AGENT_NAME = "PyDatabricksSqlConnector"
7676

7777
# These two functions are pyhive legacy

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,26 @@ def _kernel_session_accepts_kwarg(name: str) -> bool:
170170
return name in params
171171

172172

173+
def _kernel_geospatial_kwargs(value: bool) -> Dict[str, bool]:
174+
"""Build the explicit native geospatial-support kwarg.
175+
176+
The value is always passed explicitly so the driver and kernel contracts
177+
cannot drift. Older kernel wheels do not declare
178+
``enable_geospatial_support`` and must fail clearly rather than silently
179+
return a different public value shape.
180+
"""
181+
if not isinstance(value, bool):
182+
raise ValueError(
183+
"enable_geospatial_support must be a bool; " f"got {type(value).__name__}"
184+
)
185+
if not _kernel_session_accepts_kwarg("enable_geospatial_support"):
186+
raise NotSupportedError(
187+
"enable_geospatial_support requires a newer databricks-sql-kernel "
188+
"wheel that exposes geospatial result representation support."
189+
)
190+
return {"enable_geospatial_support": value}
191+
192+
173193
def _kernel_telemetry_kwargs(options: Dict[str, Any]) -> Dict[str, Any]:
174194
"""Build phase-7 telemetry/system kwargs for ``databricks_sql_kernel.Session``.
175195
@@ -263,6 +283,12 @@ def __init__(
263283
# The kernel binding owns type and range validation.
264284
self._request_timeout_secs = kwargs.get("request_timeout_secs")
265285
self._max_connections = kwargs.get("max_connections")
286+
# Opt-in native GEOMETRY / GEOGRAPHY support. True requests
287+
# the canonical Arrow struct and surfaces as ``{"srid": int, "wkb":
288+
# bytes}`` through pyarrow; False requests WKT / EWKT strings. This is
289+
# intentionally separate from ``session_configuration``: it is never
290+
# forwarded to SEA.
291+
self._enable_geospatial_support = kwargs.get("enable_geospatial_support", False)
266292
# Kernel telemetry phase 7 adds binding/runtime identity and
267293
# telemetry config kwargs directly to ``databricks_sql_kernel.Session``.
268294
self._telemetry_options = kwargs.get("telemetry_options") or {}
@@ -381,6 +407,9 @@ def open_session(
381407
# kernel's ``retry_*`` kwargs. Empty when at defaults.
382408
retry_kwargs = _kernel_retry_kwargs(self._retry_options)
383409
telemetry_kwargs = _kernel_telemetry_kwargs(self._telemetry_options)
410+
geospatial_kwargs = _kernel_geospatial_kwargs(
411+
self._enable_geospatial_support
412+
)
384413
max_connections_kwargs: Dict[str, Any] = {}
385414
if _kernel_session_accepts_kwarg("max_connections"):
386415
max_connections_kwargs["max_connections"] = self._max_connections
@@ -428,6 +457,7 @@ def open_session(
428457
**tls_kwargs,
429458
**retry_kwargs,
430459
**telemetry_kwargs,
460+
**geospatial_kwargs,
431461
**max_connections_kwargs,
432462
**http_headers_kwargs,
433463
)
@@ -1015,11 +1045,9 @@ def get_tables(
10151045
if self._kernel_session is None:
10161046
raise InterfaceError("get_tables requires an open session.")
10171047
try:
1018-
# ``table_types`` is filtered kernel-side (the kernel applies
1019-
# it to the reshaped result, case-insensitively as of the
1020-
# batch-3 kernel change), so we forward it and let the kernel
1021-
# do the work — no connector-side drain + refilter. Passing it
1022-
# through preserves streaming for large schemas.
1048+
# The kernel filters ``table_types`` with case-sensitive
1049+
# matching for Thrift parity. Forward the values unchanged
1050+
# to preserve those semantics and streaming for large schemas.
10231051
stream = self._kernel_session.metadata().list_tables(
10241052
catalog=escape_metadata_catalog_name(
10251053
_catalog_or_none(catalog_name), self._escape_catalog_name

‎src/databricks/sql/backend/thrift_backend.py‎

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import errno
44
import logging
55
import math
6+
import re
67
import time
78
import threading
89
from typing import Dict, List, Optional, Union, Any, TYPE_CHECKING
@@ -807,38 +808,50 @@ def _col_to_description(col, field=None, host_url=None):
807808
else:
808809
precision, scale = None, None
809810

810-
# Extract variant type from field if available
811+
# Extract logical types that Thrift surfaces as strings from Arrow metadata.
811812
if field is not None:
812813
try:
813-
# Check for variant type in metadata
814814
if field.metadata and b"Spark:DataType:SqlName" in field.metadata:
815815
sql_type = field.metadata.get(b"Spark:DataType:SqlName")
816-
if sql_type == b"VARIANT":
817-
cleaned_type = "variant"
816+
logical_type = re.fullmatch(
817+
rb"(variant|geometry|geography)(?:\([^()]*\))?",
818+
sql_type.strip(),
819+
re.IGNORECASE,
820+
)
821+
if logical_type:
822+
cleaned_type = logical_type.group(1).decode("ascii").lower()
818823
except Exception as e:
819-
logger.debug(f"Could not extract variant type from field: {e}")
824+
logger.debug(f"Could not extract logical type from field: {e}")
820825

821826
return col.columnName, cleaned_type, None, None, precision, scale, None
822827

823828
@staticmethod
824829
def _hive_schema_to_description(t_table_schema, schema_bytes=None, host_url=None):
830+
arrow_fields = []
825831
field_dict = {}
826832
if pyarrow and schema_bytes:
827833
try:
828834
arrow_schema = pyarrow.ipc.read_schema(pyarrow.py_buffer(schema_bytes))
829-
# Build a dictionary mapping column names to fields
830-
for field in arrow_schema:
835+
arrow_fields = list(arrow_schema)
836+
# Retain a name fallback for malformed/partial schemas, but use
837+
# result-column position whenever it is available. SQL permits
838+
# duplicate aliases, for which a name lookup is ambiguous.
839+
for field in arrow_fields:
831840
field_dict[field.name] = field
832841
except Exception as e:
833842
logger.debug(f"Could not parse arrow schema: {e}")
834843

835844
return [
836845
ThriftDatabricksClient._col_to_description(
837846
col,
838-
field_dict.get(col.columnName) if field_dict else None,
847+
(
848+
arrow_fields[index]
849+
if index < len(arrow_fields)
850+
else field_dict.get(col.columnName)
851+
),
839852
host_url,
840853
)
841-
for col in t_table_schema.columns
854+
for index, col in enumerate(t_table_schema.columns)
842855
]
843856

844857
def _results_message_to_execute_response(

‎src/databricks/sql/client.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,13 @@ def __init__(
194194
decision. This is an intentional divergence from the
195195
Thrift/SEA paths, where an explicit ``True`` can still
196196
be suppressed by the feature flag.
197+
:param enable_geospatial_support: `bool`, optional (default is False)
198+
Kernel backend only. Controls the public representation of
199+
``GEOMETRY`` and ``GEOGRAPHY`` result values. ``True`` returns
200+
``{"srid": int, "wkb": bytes}``; ``False`` returns WKT / EWKT
201+
strings (for example ``"SRID=4326;POINT(1 2)"``). The
202+
conversion is local to the kernel/driver and this option is
203+
never sent to the SQL Execution API.
197204
:param use_hybrid_disposition: `bool`, optional (default is False)
198205
Use the hybrid disposition instead of the inline disposition.
199206
:param server_hostname: Databricks instance host name.

‎src/databricks/sql/session.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,9 @@ def _create_backend(
332332
retry_options=kernel_retry_options,
333333
request_timeout_secs=kwargs.get("_socket_timeout"),
334334
max_connections=kwargs.get("_pool_maxsize") or None,
335+
enable_geospatial_support=kwargs.get(
336+
"enable_geospatial_support", False
337+
),
335338
telemetry_options=kernel_telemetry_options,
336339
escape_catalog_name=kwargs.get("escape_catalog_name", False),
337340
)

0 commit comments

Comments
 (0)