Skip to content

Commit f773dca

Browse files
committed
refactor: address review feedback on object-dtype handling
Infer object-dtype columns directly with pyarrow instead of sampling and coercing: strings map to string, other representable scalars to their Arrow type, an all-null column stays null, and a column mixing incompatible types raises rather than being silently coerced to string. Derive the int64 pruning-bound limits from np.iinfo(np.int64) rather than hardcoded literals, and drop the inaccurate "rare" comments on the cftime data-variable path.
1 parent 83e6939 commit f773dca

3 files changed

Lines changed: 21 additions & 39 deletions

File tree

tests/test_df.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ def test_compute_chunks_tuples_sum_to_dim_size():
536536
assert sum(tup) == ds.sizes[dim]
537537

538538

539-
# -- Object-dtype and out-of-ns-range coordinate support (issue #223) -------
539+
# -- Object-dtype and out-of-ns-range coordinate support --------------------
540540

541541

542542
def _field_type(schema, name):
@@ -584,18 +584,17 @@ def test_partition_metadata_skips_out_of_ns_datetime():
584584
assert all("time" not in m for m in meta)
585585

586586

587-
def test_parse_schema_all_null_object_var_defaults_to_string():
588-
# An all-null (or empty) object column has no data to infer from; object
589-
# dtype in xarray almost always means strings, so default to pa.string()
590-
# rather than a null column.
587+
def test_parse_schema_all_null_object_var_stays_null():
588+
# An all-null object column has no data to infer a type from; let null be
589+
# null rather than coercing it to a string column.
591590
ds = _ensure_default_indexes(
592591
xr.Dataset(
593592
{"label": (["x"], np.array([None, None], dtype=object))},
594593
coords={"x": [1, 2]},
595594
)
596595
)
597596
schema = _parse_schema(ds)
598-
assert schema.field("label").type == pa.string()
597+
assert pa.types.is_null(schema.field("label").type)
599598

600599

601600
def test_partition_metadata_prunes_cftime_coord():

xarray_sql/cftime.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,6 @@ def convert_for_field(values, field: pa.Field) -> np.ndarray:
170170
# ---------------------------------------------------------------------------
171171

172172

173-
#: Bounds of a signed 64-bit integer; a pruning bound outside this range
174-
#: cannot be handed to the Rust ``ScalarBound`` layer.
175-
_INT64_MIN: int = -(2**63)
176-
_INT64_MAX: int = 2**63 - 1
177-
178-
179173
def partition_bounds(
180174
values,
181175
) -> tuple[int, int, str] | None:
@@ -195,7 +189,8 @@ def partition_bounds(
195189
if is_gregorian_like(cal):
196190
us = to_microseconds(values)
197191
lo, hi = int(us.min()) * 1_000, int(us.max()) * 1_000
198-
if lo < _INT64_MIN or hi > _INT64_MAX:
192+
int64 = np.iinfo(np.int64)
193+
if lo < int64.min or hi > int64.max:
199194
return None
200195
return lo, hi, "timestamp_ns"
201196
offsets = to_offsets(values, DEFAULT_UNITS, cal)

xarray_sql/df.py

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -386,29 +386,18 @@ def iter_record_batches(
386386
yield pa.RecordBatch.from_arrays(arrays, schema=schema)
387387

388388

389-
def _arrow_type_for_object(
390-
values: np.ndarray, sample_size: int = 100
391-
) -> pa.DataType:
389+
def _arrow_type_for_object(values: np.ndarray) -> pa.DataType:
392390
"""Infer an Arrow type for a non-cftime object-dtype array.
393391
394-
``pa.from_numpy_dtype`` cannot map numpy object dtype and raises
395-
``ArrowNotImplementedError: Unsupported numpy type 17`` -- which is exactly
396-
what a string variable or coordinate produces. Object-dtype arrays are
397-
never Dask/Zarr-backed, so letting Arrow infer the type from the data with
398-
``pa.array`` (e.g. ``pa.string()`` for Python strings) is safe and triggers
399-
no remote I/O.
400-
401-
Only the first ``sample_size`` elements are inspected so a huge in-memory
402-
object column is not fully copied just to read its type. An empty or
403-
all-null sample yields ``pa.null()``, which is meaningless as a column
404-
type; object dtype in xarray almost always means strings, so fall back to
405-
``pa.string()`` in that case.
392+
``pa.from_numpy_dtype`` cannot map numpy object dtype, so let pyarrow infer
393+
the type from the data instead: strings become ``pa.string()``, bytes
394+
``pa.binary()``, and other representable Python scalars their Arrow
395+
equivalent. An all-null array stays ``pa.null()``, and a column mixing
396+
incompatible types (e.g. str and int) raises, surfacing a clear error
397+
rather than a silent coercion. Object-dtype arrays are never Dask/Zarr
398+
backed, so this triggers no remote I/O.
406399
"""
407-
sample = np.asarray(values).ravel()[:sample_size]
408-
arrow_type = pa.array(sample).type
409-
if pa.types.is_null(arrow_type):
410-
return pa.string()
411-
return arrow_type
400+
return pa.array(np.asarray(values).ravel()).type
412401

413402

414403
def _parse_schema(ds: xr.Dataset) -> pa.Schema:
@@ -448,19 +437,18 @@ def _parse_schema(ds: xr.Dataset) -> pa.Schema:
448437
columns.append(pa.field(coord_name, pa_type))
449438

450439
for var_name, var in ds.data_vars.items():
451-
# Data variables are virtually never cftime, but check dtype as a
452-
# cheap guard. Only fall back to _is_cftime (which materializes
453-
# element 0) when dtype is object.
440+
# An object-dtype data variable may hold cftime objects (encode it like
441+
# a cftime coordinate) or strings/other Python scalars (infer the Arrow
442+
# type from the data). The dtype check keeps the common numeric path off
443+
# the object branch.
454444
if var.dtype == np.dtype("O"):
455445
if cft.is_cftime(var.values):
456-
# Rare: a data variable holding cftime objects. Use same
457-
# encoding as the first cftime dimension coordinate, or default.
446+
# Encode with the same units/calendar as a cftime coordinate.
458447
cal = var.values.ravel()[0].calendar
459448
columns.append(
460449
cft.arrow_field(var_name, cft.DEFAULT_UNITS, cal)
461450
)
462451
else:
463-
# String / other object data variable.
464452
arrow_type = _arrow_type_for_object(var.values)
465453
columns.append(pa.field(var_name, arrow_type))
466454
else:

0 commit comments

Comments
 (0)