Skip to content

Commit 68a238d

Browse files
committed
fix: keep NaN distinct from NULL in float columns when pandas is enabled
With pandas enabled (the default), _convert_arrow_table maps float32 and float64 columns to pandas' nullable Float32Dtype/Float64Dtype. Converting an Arrow float column to those dtypes turns IEEE NaN into pd.NA, and to_numpy(na_value=None) then returns None for it, so a NaN value could not be told apart from SQL NULL. With _disable_pandas=True the same data already came back as float('nan'). After building the object array, set NaN back on the cells that are NaN in the Arrow column, using a vectorized pyarrow.compute.is_nan mask. NULL stays None, other values are unchanged, and tables without float columns take no extra work. Signed-off-by: Maha Rana Yadavalli <271375718+maharanay22@users.noreply.github.com>
1 parent 61b9a7f commit 68a238d

3 files changed

Lines changed: 57 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Release History
22

3+
# Unreleased
4+
- Fix: with pandas enabled (the default), a floating-point NaN in a FLOAT or DOUBLE column is now returned as `float('nan')` instead of `None`, so it can be told apart from SQL NULL. This matches what `_disable_pandas=True` already returned.
5+
36
# 4.6.0 (2026-09-24)
47
- Upgrade Databricks SQL Kernel to 1.1.0; the kernel dependency is now stable and no longer experimental.
58
- Transparently auto-recover Thrift connections to Reyden / Real-Time warehouses: when a warehouse rejects the default Thrift protocol (SQLSTATE `KP001`), the session is re-opened on the kernel backend and the warehouse is remembered so later connections skip Thrift. Applies only when no backend was chosen explicitly.

‎src/databricks/sql/result_set.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
try:
1010
import pyarrow
11+
import pyarrow.compute
1112
except ImportError:
1213
pyarrow = None
1314

@@ -130,6 +131,14 @@ def _convert_arrow_table(self, table):
130131
)
131132

132133
res = df.to_numpy(na_value=None, dtype="object")
134+
135+
# The nullable float dtypes turn IEEE NaN into NA, which would make it
136+
# indistinguishable from SQL NULL, so restore NaN values from Arrow.
137+
for i, field in enumerate(table_renamed.schema):
138+
if pyarrow.types.is_floating(field.type):
139+
is_nan = pyarrow.compute.is_nan(table_renamed.column(i))
140+
res[is_nan.fill_null(False).to_numpy(), i] = float("nan")
141+
133142
return [ResultRow(*v) for v in res]
134143

135144
@property

‎tests/unit/test_pandas_compatibility.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
import datetime
10+
import math
1011
import unittest
1112
from decimal import Decimal
1213
from unittest.mock import Mock
@@ -122,6 +123,50 @@ def test_float_types(self):
122123
self.assertIsNone(rows[1].float32_col)
123124
self.assertAlmostEqual(rows[2].float64_col, 4.5)
124125

126+
def _nan_and_null_table(self):
127+
values = [[float("nan"), None], [float("inf"), float("-inf"), 1.5]]
128+
table = pa.table(
129+
{
130+
"id": pa.chunked_array([[1, 2], [3, 4, 5]], type=pa.int64()),
131+
"float32_col": pa.chunked_array(values, type=pa.float32()),
132+
"float64_col": pa.chunked_array(values, type=pa.float64()),
133+
}
134+
)
135+
description = [
136+
("id", "bigint", None, None, None, None, None),
137+
("float32_col", "float", None, None, None, None, None),
138+
("float64_col", "double", None, None, None, None, None),
139+
]
140+
return table, description
141+
142+
def test_float_nan_is_not_converted_to_null(self):
143+
table, description = self._nan_and_null_table()
144+
145+
rows = _make_result_set(description)._convert_arrow_table(table)
146+
147+
self.assertEqual([row.id for row in rows], [1, 2, 3, 4, 5])
148+
for column in ("float32_col", "float64_col"):
149+
values = [getattr(row, column) for row in rows]
150+
self.assertIsInstance(values[0], float)
151+
self.assertTrue(math.isnan(values[0]))
152+
self.assertEqual(values[1:], [None, float("inf"), float("-inf"), 1.5])
153+
154+
def test_float_nan_and_null_match_disable_pandas_path(self):
155+
table, description = self._nan_and_null_table()
156+
157+
def normalized(rows):
158+
return [
159+
["NaN" if isinstance(v, float) and math.isnan(v) else v for v in row]
160+
for row in rows
161+
]
162+
163+
with_pandas = _make_result_set(description)._convert_arrow_table(table)
164+
without_pandas = _make_result_set(
165+
description, disable_pandas=True
166+
)._convert_arrow_table(table)
167+
168+
self.assertEqual(normalized(with_pandas), normalized(without_pandas))
169+
125170
def test_boolean_type(self):
126171
table = pa.table(
127172
{

0 commit comments

Comments
 (0)