Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: Remove depth limit for nested collection types and improve test…
… coverage

- Remove 2-level depth restriction from Array and Set constructors
  to support unbounded nesting per maintainer request
- Make _convert_nested_collection_to_proto() recursive for 3+ levels
- Update error message for nested type inference to guide users
  toward explicit Field dtype declaration
- Add 3+ level tests for Field roundtrip, str roundtrip, and PyArrow conversion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: soojin <soojin@dable.io>
  • Loading branch information
2 people authored and ntkathole committed Apr 2, 2026
commit 4dabd1b79614f8a40ba720fba87cf22245ecb53a
15 changes: 12 additions & 3 deletions sdk/python/feast/type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,8 +323,9 @@ def python_type_to_feast_value_type(
if not recurse:
raise ValueError(
f"Value type for field {name} is {type(value)} but "
f"recursion is not allowed. Array types can only be one level "
f"deep."
f"recursion is not allowed. Nested collection types cannot be "
f"inferred automatically; use an explicit Field dtype instead "
f"(e.g., dtype=Array(Array(Int32)))."
)

# This is the final type which we infer from the list
Expand Down Expand Up @@ -1109,8 +1110,16 @@ def _convert_nested_collection_to_proto(
if len(inner_list) == 0:
# Empty inner collection: store as empty ProtoValue
inner_values.append(ProtoValue())
elif any(
isinstance(item, (list, set, tuple)) for item in inner_list
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
):
# Deeper nesting (3+ levels): recurse
inner_proto = _convert_nested_collection_to_proto(
feast_value_type, [inner_list]
)
inner_values.append(inner_proto[0])
else:
# Wrap the inner list as a single list-typed Value
# Leaf level: wrap as a single list-typed Value
proto_vals = python_values_to_proto_values(
[inner_list], ValueType.UNKNOWN
)
Expand Down
25 changes: 5 additions & 20 deletions sdk/python/feast/types.py
Comment thread
soooojinlee marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -177,17 +177,10 @@ class Array(ComplexFeastType):

def __init__(self, base_type: Union[PrimitiveFeastType, "ComplexFeastType"]):
# Allow Struct, Array, and Set as base types for nested collections
if isinstance(base_type, (Struct, Array, Set)):
# Enforce 2-level depth limit: reject Array(Array(Array(...))) etc.
if isinstance(base_type, (Array, Set)) and isinstance(
base_type.base_type, (Array, Set)
):
raise ValueError(
f"Nested collection types are limited to 2 levels of nesting. "
f"{type(base_type).__name__}({type(base_type.base_type).__name__}(...)) "
f"is too deeply nested."
)
elif base_type not in SUPPORTED_BASE_TYPES:
if (
not isinstance(base_type, (Struct, Array, Set))
and base_type not in SUPPORTED_BASE_TYPES
):
raise ValueError(
f"Type {type(base_type)} is currently not supported as a base type for Array."
)
Expand Down Expand Up @@ -230,15 +223,7 @@ class Set(ComplexFeastType):

def __init__(self, base_type: Union[PrimitiveFeastType, ComplexFeastType]):
# Allow Array and Set as base types for nested collections
if isinstance(base_type, (Array, Set)):
# Enforce 2-level depth limit
if isinstance(base_type.base_type, (Array, Set)):
raise ValueError(
f"Nested collection types are limited to 2 levels of nesting. "
f"{type(base_type).__name__}({type(base_type.base_type).__name__}(...)) "
f"is too deeply nested."
)
else:
if not isinstance(base_type, (Array, Set)):
# Sets do not support MAP as a base type
supported_set_types = [t for t in SUPPORTED_BASE_TYPES if t not in (Map,)]
if base_type not in supported_set_types:
Expand Down
38 changes: 28 additions & 10 deletions sdk/python/tests/unit/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,24 @@ def test_nested_set_set():
assert from_feast_type(t) == ValueType.SET_SET


def test_nested_depth_limit():
"""3 levels of nesting should raise ValueError."""
with pytest.raises(ValueError, match="too deeply nested"):
Array(Array(Array(String)))
def test_nested_unbounded_depth():
"""Nesting depth should be unbounded."""
# 3-level
t3 = Array(Array(Array(String)))
assert t3.to_value_type() == ValueType.LIST_LIST

with pytest.raises(ValueError, match="too deeply nested"):
Array(Set(Array(String)))
t3_mixed = Array(Set(Array(String)))
assert t3_mixed.to_value_type() == ValueType.LIST_SET

with pytest.raises(ValueError, match="too deeply nested"):
Set(Array(Array(String)))
t3_set = Set(Array(Array(String)))
assert t3_set.to_value_type() == ValueType.SET_LIST

with pytest.raises(ValueError, match="too deeply nested"):
Set(Set(Set(String)))
t3_set2 = Set(Set(Set(String)))
assert t3_set2.to_value_type() == ValueType.SET_SET

# 4-level
t4 = Array(Array(Array(Array(Int32))))
assert t4.to_value_type() == ValueType.LIST_LIST


def test_nested_from_value_type_roundtrip():
Expand Down Expand Up @@ -129,6 +134,10 @@ def test_nested_pyarrow_conversion():
pa_type = from_feast_to_pyarrow_type(Set(Set(Bool)))
assert pa_type == pyarrow.list_(pyarrow.list_(pyarrow.bool_()))

# 3-level: Array(Array(Array(Int32))) -> list(list(list(int32)))
pa_type = from_feast_to_pyarrow_type(Array(Array(Array(Int32))))
assert pa_type == pyarrow.list_(pyarrow.list_(pyarrow.list_(pyarrow.int32())))


def test_nested_field_roundtrip():
"""Field with nested collection type should survive to_proto -> from_proto."""
Expand All @@ -137,6 +146,11 @@ def test_nested_field_roundtrip():
("as_field", Array(Set(Int32))),
("sa", Set(Array(Float64))),
("ss", Set(Set(Bool))),
# 3-level nesting
("aaa", Array(Array(Array(Int32)))),
("asa", Array(Set(Array(String)))),
# 4-level nesting
("aaaa", Array(Array(Array(Array(Float64))))),
]
for name, dtype in test_cases:
field = Field(name=name, dtype=dtype, tags={"user_tag": "value"})
Expand Down Expand Up @@ -196,6 +210,10 @@ def test_feast_type_str_roundtrip():
Set(Array(Float32)),
Set(Set(Int32)),
Set(Set(Float64)),
# 3+ level nesting
Array(Array(Array(String))),
Array(Set(Array(Int32))),
Set(Set(Set(Float64))),
]
for dtype in test_cases:
s = _feast_type_to_str(dtype)
Expand Down