Visitar URL original
fix: ambiguous truth value of array during materialization · feast-dev/feast@33d1613 · GitHub
Skip to content

Commit 33d1613

Browse files
fix: ambiguous truth value of array during materialization
Signed-off-by: Alan Gauthier <alan.gauthier@jobteaser.com>
1 parent 0c469a7 commit 33d1613

2 files changed

Lines changed: 159 additions & 13 deletions

File tree

‎sdk/python/feast/type_map.py‎

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -929,16 +929,35 @@ def _convert_scalar_values_to_proto(
929929
return [ProtoValue()] * len(values)
930930

931931
if feast_value_type == ValueType.UNIX_TIMESTAMP:
932-
int_timestamps = _python_datetime_to_int_timestamp(values)
933-
return [ProtoValue(unix_timestamp_val=ts) for ts in int_timestamps] # type: ignore
932+
out = []
933+
for value in values:
934+
if isinstance(value, np.ndarray) or (
935+
hasattr(value, "__len__") and not isinstance(value, (str, bytes))
936+
):
937+
# Array-like value in a scalar UNIX_TIMESTAMP column: treat as null.
938+
out.append(ProtoValue())
939+
elif value is None:
940+
out.append(ProtoValue())
941+
else:
942+
(ts,) = _python_datetime_to_int_timestamp([value])
943+
out.append(ProtoValue(unix_timestamp_val=ts)) # type: ignore
944+
return out
934945

935946
field_name, func, valid_scalar_types = PYTHON_SCALAR_VALUE_TYPE_TO_PROTO_VALUE[
936947
feast_value_type
937948
]
938949

939-
# Validate scalar types
940-
if valid_scalar_types:
941-
if (sample == 0 or sample == 0.0) and feast_value_type != ValueType.BOOL:
950+
# Validate scalar types — skip for array-like samples (they will be treated
951+
# as null or raw values in the conversion loop below).
952+
if valid_scalar_types and not (
953+
isinstance(sample, np.ndarray)
954+
or (hasattr(sample, "__len__") and not isinstance(sample, (str, bytes)))
955+
):
956+
try:
957+
is_zero = sample == 0 or sample == 0.0
958+
except (ValueError, TypeError):
959+
is_zero = False
960+
if is_zero and feast_value_type != ValueType.BOOL:
942961
# Numpy converts 0 to int, but column type may be float
943962
allowed_types = {np.int64, int, np.float64, float, decimal.Decimal}
944963
assert type(sample) in allowed_types, (
@@ -951,20 +970,39 @@ def _convert_scalar_values_to_proto(
951970

952971
# Handle BOOL specially due to np.bool_ conversion requirement
953972
if feast_value_type == ValueType.BOOL:
954-
return [
955-
ProtoValue(
956-
**{field_name: func(bool(value) if type(value) is np.bool_ else value)}
957-
) # type: ignore
958-
if not pd.isnull(value)
959-
else ProtoValue()
960-
for value in values
961-
]
973+
out = []
974+
for value in values:
975+
if isinstance(value, np.ndarray) or (
976+
hasattr(value, "__len__") and not isinstance(value, (str, bytes))
977+
):
978+
# Array-like value in a scalar BOOL column: treat as null.
979+
out.append(ProtoValue())
980+
elif not pd.isnull(value):
981+
out.append(
982+
ProtoValue(
983+
**{
984+
field_name: func(
985+
bool(value) if type(value) is np.bool_ else value
986+
)
987+
}
988+
) # type: ignore
989+
)
990+
else:
991+
out.append(ProtoValue())
992+
return out
962993

963994
# Generic scalar conversion
964995
out = []
965996
for value in values:
966997
if isinstance(value, ProtoValue):
967998
out.append(value)
999+
elif isinstance(value, np.ndarray) or (
1000+
hasattr(value, "__len__") and not isinstance(value, (str, bytes))
1001+
):
1002+
# Array-like value in a scalar column: always treat as null.
1003+
# pd.isnull() is vectorised and would return an ndarray here,
1004+
# making `not pd.isnull(value)` raise ValueError.
1005+
out.append(ProtoValue())
9681006
elif not pd.isnull(value):
9691007
out.append(ProtoValue(**{field_name: func(value)}))
9701008
else:

‎sdk/python/tests/unit/test_type_map.py‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,3 +1845,111 @@ def test_pa_to_feast_value_type_nested(self):
18451845
pa_to_feast_value_type("list<item: list<item: double>>")
18461846
== ValueType.VALUE_LIST
18471847
)
1848+
1849+
1850+
class TestEmptyArrayAsNull:
1851+
"""Regression tests for https://github.com/feast-dev/feast/issues/6255
1852+
Ensure that an empty numpy array in a scalar feature column is treated as
1853+
null rather than raising ``ValueError: The truth value of an empty array is
1854+
ambiguous``.
1855+
"""
1856+
1857+
def test_empty_numpy_array_treated_as_null_double(self):
1858+
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue
1859+
1860+
result = python_values_to_proto_values(
1861+
[np.array([]), 1.0, None], ValueType.DOUBLE
1862+
)
1863+
assert result[0] == ProtoValue(), (
1864+
"empty array should produce an empty ProtoValue"
1865+
)
1866+
assert result[1].double_val == 1.0
1867+
assert result[2] == ProtoValue(), (
1868+
"None should still produce an empty ProtoValue"
1869+
)
1870+
1871+
def test_empty_numpy_array_treated_as_null_int64(self):
1872+
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue
1873+
1874+
result = python_values_to_proto_values(
1875+
[np.array([]), 42, None], ValueType.INT64
1876+
)
1877+
assert result[0] == ProtoValue(), (
1878+
"empty array should produce an empty ProtoValue"
1879+
)
1880+
assert result[1].int64_val == 42
1881+
assert result[2] == ProtoValue()
1882+
1883+
def test_empty_numpy_array_treated_as_null_bool(self):
1884+
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue
1885+
1886+
result = python_values_to_proto_values(
1887+
[np.array([]), True, None], ValueType.BOOL
1888+
)
1889+
assert result[0] == ProtoValue(), (
1890+
"empty array should produce an empty ProtoValue"
1891+
)
1892+
assert result[1].bool_val is True
1893+
assert result[2] == ProtoValue()
1894+
1895+
def test_array_with_null_element_treated_as_null(self):
1896+
"""A non-empty array containing any null element in a scalar column is treated as null."""
1897+
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue
1898+
1899+
result = python_values_to_proto_values(
1900+
[np.array([np.nan, 1.0]), 3.0], ValueType.DOUBLE
1901+
)
1902+
assert result[0] == ProtoValue(), (
1903+
"array with null element should produce an empty ProtoValue"
1904+
)
1905+
assert result[1].double_val == 3.0
1906+
1907+
def test_non_empty_array_without_nulls_is_treated_as_null(self):
1908+
"""A non-empty numpy array in a scalar column is always treated as null.
1909+
1910+
A scalar feature column cannot hold an ndarray value (protobuf would
1911+
reject it), so any array-like value – empty or not – is mapped to an
1912+
empty ProtoValue() rather than crashing with ValueError.
1913+
"""
1914+
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue
1915+
1916+
result = python_values_to_proto_values(
1917+
[np.array([1.0, 2.0]), 3.0, None], ValueType.DOUBLE
1918+
)
1919+
# array-like value in a scalar column → null, not a crash
1920+
assert result[0] == ProtoValue(), (
1921+
"non-empty array in scalar column should be null"
1922+
)
1923+
assert result[1].double_val == 3.0
1924+
assert result[2] == ProtoValue()
1925+
1926+
def test_empty_numpy_array_treated_as_null_unix_timestamp(self):
1927+
"""Array-like values in a scalar UNIX_TIMESTAMP column must not crash."""
1928+
from datetime import datetime, timezone
1929+
1930+
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue
1931+
1932+
ts = datetime(2024, 1, 1, tzinfo=timezone.utc)
1933+
result = python_values_to_proto_values(
1934+
[np.array([]), ts, None], ValueType.UNIX_TIMESTAMP
1935+
)
1936+
assert result[0] == ProtoValue(), (
1937+
"empty array in UNIX_TIMESTAMP scalar column should produce null"
1938+
)
1939+
assert result[1].unix_timestamp_val == int(ts.timestamp())
1940+
assert result[2] == ProtoValue()
1941+
1942+
def test_non_empty_array_treated_as_null_unix_timestamp(self):
1943+
"""Non-empty array in a UNIX_TIMESTAMP scalar column should produce null, not crash."""
1944+
from datetime import datetime, timezone
1945+
1946+
from feast.protos.feast.types.Value_pb2 import Value as ProtoValue
1947+
1948+
ts = datetime(2024, 6, 15, tzinfo=timezone.utc)
1949+
result = python_values_to_proto_values(
1950+
[np.array([1, 2, 3]), ts], ValueType.UNIX_TIMESTAMP
1951+
)
1952+
assert result[0] == ProtoValue(), (
1953+
"non-empty array in UNIX_TIMESTAMP scalar column should produce null"
1954+
)
1955+
assert result[1].unix_timestamp_val == int(ts.timestamp())

0 commit comments

Comments
 (0)