Expected Behavior
get_historical_features with an entity_df that happens to have zero rows should
return an empty result with the right columns. A zero-row entity frame is a normal
degenerate case in batch scoring — the upstream query simply matched nothing that run.
Current Behavior
It raises an opaque pandas error from deep inside the Dask offline store:
TypeError: Invalid comparison between dtype=datetime64[ns] and DatetimeArray
Traceback tail:
File "sdk/python/feast/infra/offline_stores/offline_store.py", line 178, in to_arrow
features_table = self._to_arrow_internal(timeout=timeout)
File "sdk/python/feast/infra/offline_stores/dask.py", line 103, in _to_arrow_internal
df = self.evaluation_function().compute()
File "sdk/python/feast/infra/offline_stores/dask.py", line 317, in evaluate_historical_retrieval
df_to_join = _filter_ttl(
File "sdk/python/feast/infra/offline_stores/dask.py", line 1206, in _filter_ttl
df_to_join = df_to_join.persist()
Steps to reproduce
Identical construction, only the row count differs, against a file offline store:
def mk(n):
return pd.DataFrame({
"driver_id": [1001] * n,
"event_timestamp": pd.to_datetime([datetime(2026, 2, 1)] * n, utc=True),
})
fs.get_historical_features(entity_df=mk(1), features=["driver_stats:conv_rate"]).to_df()
# OK, shape=(1, 3)
fs.get_historical_features(entity_df=mk(0), features=["driver_stats:conv_rate"]).to_df()
# TypeError: Invalid comparison between dtype=datetime64[ns] and DatetimeArray
Specifications
- Version:
master @ 5ad5592390febfca60c9d88edf7daccbdd156fd6
- Platform: Linux x86_64, Python 3.11.15, dask 2026.8.0
- Subsystem: offline store (Dask / file)
Possible Solution
_normalize_timestamp (sdk/python/feast/infra/offline_stores/dask.py:1139) makes
timestamp columns tz-aware with a row-wise apply:
df_to_join[timestamp_field] = df_to_join[timestamp_field].apply(
lambda x: x if x.tzinfo else x.replace(tzinfo=timezone.utc),
meta=(timestamp_field, "datetime64[ns, UTC]"),
)
meta declares a tz-aware result, but with zero rows the lambda never runs, so the
computed partition stays datetime64[ns]. Declared and actual dtypes then diverge, and
the tz-naive vs tz-aware comparison in _filter_ttl
(sdk/python/feast/infra/offline_stores/dask.py:1183) raises.
Replacing the row-wise apply with a vectorized, empty-safe conversion fixes it and is
faster on non-empty frames too: pick dt.tz_localize("UTC") for a tz-naive column and
dt.tz_convert("UTC") otherwise, based on the column's declared dtype. That yields the
correct dtype even when the partition is empty.
A regression test covering a zero-row entity_df through
get_historical_features(...).to_df() on the Dask store would pin this down; the
current suite only exercises non-empty entity frames, which is why CI stays green.
Happy to send a PR for this.
Expected Behavior
get_historical_featureswith anentity_dfthat happens to have zero rows shouldreturn an empty result with the right columns. A zero-row entity frame is a normal
degenerate case in batch scoring — the upstream query simply matched nothing that run.
Current Behavior
It raises an opaque pandas error from deep inside the Dask offline store:
Traceback tail:
Steps to reproduce
Identical construction, only the row count differs, against a
fileoffline store:Specifications
master@5ad5592390febfca60c9d88edf7daccbdd156fd6Possible Solution
_normalize_timestamp(sdk/python/feast/infra/offline_stores/dask.py:1139) makestimestamp columns tz-aware with a row-wise
apply:metadeclares a tz-aware result, but with zero rows the lambda never runs, so thecomputed partition stays
datetime64[ns]. Declared and actual dtypes then diverge, andthe tz-naive vs tz-aware comparison in
_filter_ttl(
sdk/python/feast/infra/offline_stores/dask.py:1183) raises.Replacing the row-wise
applywith a vectorized, empty-safe conversion fixes it and isfaster on non-empty frames too: pick
dt.tz_localize("UTC")for a tz-naive column anddt.tz_convert("UTC")otherwise, based on the column's declared dtype. That yields thecorrect dtype even when the partition is empty.
A regression test covering a zero-row
entity_dfthroughget_historical_features(...).to_df()on the Dask store would pin this down; thecurrent suite only exercises non-empty entity frames, which is why CI stays green.
Happy to send a PR for this.