Skip to content

Commit 82d580f

Browse files
committed
check for case-insensitive matches across types
1 parent a2a0e29 commit 82d580f

File tree

3 files changed

+92
-38
lines changed

3 files changed

+92
-38
lines changed

sdk/python/feast/infra/registry/base_registry.py

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from feast.entity import Entity
2727
from feast.errors import (
2828
ConflictingFeatureViewNames,
29-
FeatureViewNotFoundException,
3029
)
3130
from feast.feature_service import FeatureService
3231
from feast.feature_view import FeatureView
@@ -281,46 +280,17 @@ def _ensure_feature_view_name_is_unique(
281280
The primary validation happens in _validate_all_feature_views() during feast plan/apply.
282281
"""
283282
name = feature_view.name
283+
case_insensitive_name = name.lower()
284284
new_type = type(feature_view).__name__
285+
for existing_feature_view in self.list_all_feature_views(
286+
project, allow_cache=allow_cache
287+
):
288+
if existing_feature_view.name.lower() != case_insensitive_name:
289+
continue
285290

286-
def _check_conflict(getter, not_found_exc, existing_type: str):
287-
try:
288-
getter(name, project, allow_cache=allow_cache)
291+
existing_type = type(existing_feature_view).__name__
292+
if existing_type != new_type:
289293
raise ConflictingFeatureViewNames(name, existing_type, new_type)
290-
except not_found_exc:
291-
pass
292-
293-
# Check StreamFeatureView before FeatureView since StreamFeatureView is a subclass
294-
# Note: All getters raise FeatureViewNotFoundException (not type-specific exceptions)
295-
if isinstance(feature_view, StreamFeatureView):
296-
_check_conflict(
297-
self.get_feature_view, FeatureViewNotFoundException, "FeatureView"
298-
)
299-
_check_conflict(
300-
self.get_on_demand_feature_view,
301-
FeatureViewNotFoundException,
302-
"OnDemandFeatureView",
303-
)
304-
elif isinstance(feature_view, FeatureView):
305-
_check_conflict(
306-
self.get_stream_feature_view,
307-
FeatureViewNotFoundException,
308-
"StreamFeatureView",
309-
)
310-
_check_conflict(
311-
self.get_on_demand_feature_view,
312-
FeatureViewNotFoundException,
313-
"OnDemandFeatureView",
314-
)
315-
elif isinstance(feature_view, OnDemandFeatureView):
316-
_check_conflict(
317-
self.get_feature_view, FeatureViewNotFoundException, "FeatureView"
318-
)
319-
_check_conflict(
320-
self.get_stream_feature_view,
321-
FeatureViewNotFoundException,
322-
"StreamFeatureView",
323-
)
324294

325295
@abstractmethod
326296
def delete_feature_view(self, name: str, project: str, commit: bool = True):

sdk/python/tests/integration/registration/test_universal_registry.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2061,6 +2061,60 @@ def simple_udf(x: int):
20612061
test_registry.teardown()
20622062

20632063

2064+
@pytest.mark.integration
2065+
@pytest.mark.parametrize("test_registry", all_fixtures)
2066+
def test_cross_type_feature_view_name_conflict_case_insensitive(
2067+
test_registry: BaseRegistry,
2068+
):
2069+
"""
2070+
Test that cross-type feature view name checks are case-insensitive.
2071+
"""
2072+
project = "project"
2073+
2074+
entity = Entity(name="driver_entity", join_keys=["test_key"])
2075+
file_source = FileSource(name="my_file_source", path="test.parquet")
2076+
feature_view = FeatureView(
2077+
name="Shared_Feature_View_Name",
2078+
entities=[entity],
2079+
schema=[Field(name="feature1", dtype=Float32)],
2080+
source=file_source,
2081+
)
2082+
2083+
stream_source = KafkaSource(
2084+
name="kafka",
2085+
timestamp_field="event_timestamp",
2086+
kafka_bootstrap_servers="",
2087+
message_format=AvroFormat(""),
2088+
topic="topic",
2089+
batch_source=FileSource(path="some path"),
2090+
watermark_delay_threshold=timedelta(days=1),
2091+
)
2092+
2093+
def simple_udf(x: int):
2094+
return x + 3
2095+
2096+
stream_feature_view = StreamFeatureView(
2097+
name="shared_feature_view_name",
2098+
entities=[entity],
2099+
ttl=timedelta(days=30),
2100+
schema=[Field(name="feature1", dtype=Float32)],
2101+
source=stream_source,
2102+
udf=simple_udf,
2103+
)
2104+
2105+
test_registry.apply_feature_view(feature_view, project)
2106+
2107+
with pytest.raises(ConflictingFeatureViewNames) as exc_info:
2108+
test_registry.apply_feature_view(stream_feature_view, project)
2109+
2110+
error_message = str(exc_info.value)
2111+
assert "FeatureView" in error_message
2112+
assert "StreamFeatureView" in error_message
2113+
2114+
test_registry.delete_feature_view("Shared_Feature_View_Name", project)
2115+
test_registry.teardown()
2116+
2117+
20642118
@pytest.mark.integration
20652119
@pytest.mark.parametrize("test_registry", all_fixtures)
20662120
def test_cross_type_feature_view_odfv_conflict(test_registry: BaseRegistry):

sdk/python/tests/unit/infra/registry/test_sql_registry.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,33 @@ def test_feature_view_name_conflict_between_stream_and_batch(sqlite_registry):
105105

106106
with pytest.raises(ConflictingFeatureViewNames):
107107
sqlite_registry.apply_feature_view(stream_view, "test_project")
108+
109+
110+
def test_feature_view_name_conflict_is_case_insensitive(sqlite_registry):
111+
entity = Entity(
112+
name="driver",
113+
value_type=ValueType.STRING,
114+
join_keys=["driver_id"],
115+
)
116+
sqlite_registry.apply_entity(entity, "test_project")
117+
118+
file_source = FileSource(
119+
path="driver_stats.parquet",
120+
timestamp_field="event_timestamp",
121+
created_timestamp_column="created",
122+
)
123+
124+
batch_view = _build_feature_view("Driver_Activity", entity, file_source)
125+
sqlite_registry.apply_feature_view(batch_view, "test_project")
126+
127+
push_source = PushSource(name="driver_push", batch_source=file_source)
128+
stream_view = StreamFeatureView(
129+
name="driver_activity",
130+
source=push_source,
131+
entities=[entity],
132+
schema=[Field(name="conv_rate", dtype=Float32)],
133+
timestamp_field="event_timestamp",
134+
)
135+
136+
with pytest.raises(ConflictingFeatureViewNames):
137+
sqlite_registry.apply_feature_view(stream_view, "test_project")

0 commit comments

Comments
 (0)