diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index 76da6ad831d..42539b2e81d 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -755,7 +755,7 @@ def apply_feature_view( self._prepare_registry_for_changes(project) assert self.cached_registry_proto - self._check_conflicting_feature_view_names(feature_view) + self._ensure_feature_view_name_is_unique(feature_view, project, allow_cache=True) existing_feature_views_of_same_type: RepeatedCompositeFieldContainer if isinstance(feature_view, StreamFeatureView): existing_feature_views_of_same_type = ( @@ -1351,26 +1351,6 @@ def _get_registry_proto( return registry_proto - def _check_conflicting_feature_view_names(self, feature_view: BaseFeatureView): - name_to_fv_protos = self._existing_feature_view_names_to_fvs() - if feature_view.name in name_to_fv_protos: - if not isinstance( - name_to_fv_protos.get(feature_view.name), feature_view.proto_class - ): - raise ConflictingFeatureViewNames(feature_view.name) - - def _existing_feature_view_names_to_fvs(self) -> Dict[str, Message]: - assert self.cached_registry_proto - odfvs = { - fv.spec.name: fv - for fv in self.cached_registry_proto.on_demand_feature_views - } - fvs = {fv.spec.name: fv for fv in self.cached_registry_proto.feature_views} - sfv = { - fv.spec.name: fv for fv in self.cached_registry_proto.stream_feature_views - } - return {**odfvs, **fvs, **sfv} - def get_permission( self, name: str, project: str, allow_cache: bool = False ) -> Permission: diff --git a/sdk/python/tests/integration/registration/test_universal_registry.py b/sdk/python/tests/integration/registration/test_universal_registry.py index fb09395d789..0b3e9d85c37 100644 --- a/sdk/python/tests/integration/registration/test_universal_registry.py +++ b/sdk/python/tests/integration/registration/test_universal_registry.py @@ -2192,3 +2192,58 @@ def shared_odfv_name(inputs: pd.DataFrame) -> pd.DataFrame: # Cleanup test_registry.delete_feature_view("shared_odfv_name", project) test_registry.teardown() + + +@pytest.mark.parametrize( + "test_registry", + all_fixtures, +) +def test_cross_project_feature_view_name_allowed(test_registry: BaseRegistry): + """ + Test that different projects can use the same feature view names. + This is a regression test for issue #6209. + """ + project_a = "project_a" + project_b = "project_b" + + # Create a FeatureView in project A + feature_view_a = FeatureView( + name="shared_name", + entities=[], + schema=[Field(name="feature1", dtype=Float32)], + source=FileSource(path="data.parquet"), + ) + + # Create a StreamFeatureView with the same name in project B + stream_feature_view_b = StreamFeatureView( + name="shared_name", + entities=[], + schema=[Field(name="feature2", dtype=Float32)], + source=KafkaSource( + name="kafka_source", + kafka_bootstrap_servers="localhost:9092", + topic="test_topic", + timestamp_field="event_timestamp", + batch_source=FileSource(path="stream_data.parquet"), + ), + aggregations=[], + ) + + # Both should apply successfully without ConflictingFeatureViewNames error + test_registry.apply_feature_view(feature_view_a, project_a) + test_registry.apply_feature_view(stream_feature_view_b, project_b) + + # Verify both exist in their respective projects + retrieved_fv_a = test_registry.get_feature_view("shared_name", project_a) + assert retrieved_fv_a.name == "shared_name" + assert isinstance(retrieved_fv_a, FeatureView) + assert not isinstance(retrieved_fv_a, StreamFeatureView) + + retrieved_sfv_b = test_registry.get_stream_feature_view("shared_name", project_b) + assert retrieved_sfv_b.name == "shared_name" + assert isinstance(retrieved_sfv_b, StreamFeatureView) + + # Cleanup + test_registry.delete_feature_view("shared_name", project_a) + test_registry.delete_feature_view("shared_name", project_b) + test_registry.teardown()