Skip to content

Commit d35e16c

Browse files
fix: ambiguous truth value of array during materialization
1 parent 0c469a7 commit d35e16c

2 files changed

Lines changed: 115 additions & 11 deletions

File tree

sdk/python/feast/type_map.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -936,9 +936,17 @@ def _convert_scalar_values_to_proto(
936936
feast_value_type
937937
]
938938

939-
# Validate scalar types
940-
if valid_scalar_types:
941-
if (sample == 0 or sample == 0.0) and feast_value_type != ValueType.BOOL:
939+
# Validate scalar types — skip for array-like samples (they will be treated
940+
# as null or raw values in the conversion loop below).
941+
if valid_scalar_types and not (
942+
isinstance(sample, np.ndarray)
943+
or (hasattr(sample, "__len__") and not isinstance(sample, (str, bytes)))
944+
):
945+
try:
946+
is_zero = sample == 0 or sample == 0.0
947+
except (ValueError, TypeError):
948+
is_zero = False
949+
if is_zero and feast_value_type != ValueType.BOOL:
942950
# Numpy converts 0 to int, but column type may be float
943951
allowed_types = {np.int64, int, np.float64, float, decimal.Decimal}
944952
assert type(sample) in allowed_types, (
@@ -951,20 +959,39 @@ def _convert_scalar_values_to_proto(
951959

952960
# Handle BOOL specially due to np.bool_ conversion requirement
953961
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-
]
962+
out = []
963+
for value in values:
964+
if isinstance(value, np.ndarray) or (
965+
hasattr(value, "__len__") and not isinstance(value, (str, bytes))
966+
):
967+
# Array-like value in a scalar BOOL column: treat as null.
968+
out.append(ProtoValue())
969+
elif not pd.isnull(value):
970+
out.append(
971+
ProtoValue(
972+
**{
973+
field_name: func(
974+
bool(value) if type(value) is np.bool_ else value
975+
)
976+
}
977+
) # type: ignore
978+
)
979+
else:
980+
out.append(ProtoValue())
981+
return out
962982

963983
# Generic scalar conversion
964984
out = []
965985
for value in values:
966986
if isinstance(value, ProtoValue):
967987
out.append(value)
988+
elif isinstance(value, np.ndarray) or (
989+
hasattr(value, "__len__") and not isinstance(value, (str, bytes))
990+
):
991+
# Array-like value in a scalar column: always treat as null.
992+
# pd.isnull() is vectorised and would return an ndarray here,
993+
# making `not pd.isnull(value)` raise ValueError.
994+
out.append(ProtoValue())
968995
elif not pd.isnull(value):
969996
out.append(ProtoValue(**{field_name: func(value)}))
970997
else:

sdk/python/tests/unit/test_type_map.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,3 +1845,80 @@ 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()

0 commit comments

Comments
 (0)