From a7ff73862c46604d0e7149f8f2baf61bc13e74b5 Mon Sep 17 00:00:00 2001 From: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> Date: Tue, 5 May 2026 22:23:58 +0100 Subject: [PATCH 1/6] fix: scope feature view name conflict check to current project in file-based registry _check_conflicting_feature_view_names built its lookup map from all feature views in cached_registry_proto without filtering by project. In a shared file-based registry with multiple projects, this caused false ConflictingFeatureViewNames errors when two different projects defined feature views with the same name. Add a project parameter to both _check_conflicting_feature_view_names and _existing_feature_view_names_to_fvs, and filter each collection by fv.spec.project == project so only same-project views are compared. Fixes #6209 Signed-off-by: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> --- sdk/python/feast/infra/registry/registry.py | 21 ++++++--- .../test_local_feature_store.py | 47 +++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/sdk/python/feast/infra/registry/registry.py b/sdk/python/feast/infra/registry/registry.py index f3437b08f30..e26595ad4af 100644 --- a/sdk/python/feast/infra/registry/registry.py +++ b/sdk/python/feast/infra/registry/registry.py @@ -764,7 +764,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._check_conflicting_feature_view_names(feature_view, project) existing_feature_views_of_same_type: RepeatedCompositeFieldContainer if isinstance(feature_view, StreamFeatureView): existing_feature_views_of_same_type = ( @@ -1360,23 +1360,32 @@ 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() + def _check_conflicting_feature_view_names( + self, feature_view: BaseFeatureView, project: str + ): + name_to_fv_protos = self._existing_feature_view_names_to_fvs(project) 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]: + def _existing_feature_view_names_to_fvs(self, project: str) -> Dict[str, Message]: assert self.cached_registry_proto odfvs = { fv.spec.name: fv for fv in self.cached_registry_proto.on_demand_feature_views + if fv.spec.project == project + } + fvs = { + fv.spec.name: fv + for fv in self.cached_registry_proto.feature_views + if fv.spec.project == project } - 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 + fv.spec.name: fv + for fv in self.cached_registry_proto.stream_feature_views + if fv.spec.project == project } return {**odfvs, **fvs, **sfv} diff --git a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py index 9b7660bf692..c1fd137eeee 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py +++ b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py @@ -543,6 +543,53 @@ def test_apply_conflicting_feature_view_names(feature_store_with_local_registry) feature_store_with_local_registry.teardown() +def test_cross_project_feature_view_names_do_not_conflict(): + """Feature views with the same name in different projects must not raise ConflictingFeatureViewNames.""" + fd, registry_path = mkstemp() + fd, online_store_path = mkstemp() + + def make_store(project: str) -> FeatureStore: + return FeatureStore( + config=RepoConfig( + registry=registry_path, + project=project, + provider="local", + online_store=SqliteOnlineStoreConfig(path=online_store_path), + entity_key_serialization_version=3, + ) + ) + + store_a = make_store("project_a") + store_b = make_store("project_b") + + entity = Entity(name="driver", join_keys=["driver_id"]) + source = FileSource(path="driver_stats.parquet") + + fv_a = FeatureView( + name="driver_stats", + entities=[entity], + schema=[Field(name="driver_id", dtype=Int64)], + ttl=timedelta(seconds=10), + online=False, + source=source, + ) + store_a.apply([entity, fv_a]) + + fv_b = FeatureView( + name="driver_stats", + entities=[entity], + schema=[Field(name="driver_id", dtype=Int64)], + ttl=timedelta(seconds=10), + online=False, + source=source, + ) + # Must not raise ConflictingFeatureViewNames — same name but different project. + store_b.apply([entity, fv_b]) + + store_a.teardown() + store_b.teardown() + + @pytest.mark.parametrize( "test_feature_store", [lazy_fixture("feature_store_with_local_registry")], From 7cb8e07330fb60850fae98af4c02b91f4b526ef9 Mon Sep 17 00:00:00 2001 From: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> Date: Tue, 5 May 2026 22:29:48 +0100 Subject: [PATCH 2/6] chore: suppress detect-secrets false positive in openlineage-secret manifest The placeholder value your-marquez-api-key in openlineage-secret_v1_secret.yaml triggers detect-secrets. Mark it as an allowlist exception. Signed-off-by: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> --- .../bundle/manifests/openlineage-secret_v1_secret.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml b/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml index 40483cc0c43..a66a41e8466 100644 --- a/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml +++ b/infra/feast-operator/bundle/manifests/openlineage-secret_v1_secret.yaml @@ -3,4 +3,4 @@ kind: Secret metadata: name: openlineage-secret stringData: - api_key: your-marquez-api-key + api_key: your-marquez-api-key # pragma: allowlist secret From 6a4624afb4b38a227abf05a2d2300985d3450e14 Mon Sep 17 00:00:00 2001 From: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> Date: Tue, 5 May 2026 22:44:18 +0100 Subject: [PATCH 3/6] test: use a real temp parquet file in cross-project conflict test FileSource requires the file to exist when apply() succeeds (no early exception). Replace the bare path string with a mkstemp() path so the test passes in CI. Signed-off-by: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> --- .../tests/unit/local_feast_tests/test_local_feature_store.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py index c1fd137eeee..f54b73897f4 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py +++ b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py @@ -547,6 +547,7 @@ def test_cross_project_feature_view_names_do_not_conflict(): """Feature views with the same name in different projects must not raise ConflictingFeatureViewNames.""" fd, registry_path = mkstemp() fd, online_store_path = mkstemp() + fd, parquet_path = mkstemp(suffix=".parquet") def make_store(project: str) -> FeatureStore: return FeatureStore( @@ -563,7 +564,7 @@ def make_store(project: str) -> FeatureStore: store_b = make_store("project_b") entity = Entity(name="driver", join_keys=["driver_id"]) - source = FileSource(path="driver_stats.parquet") + source = FileSource(path=parquet_path) fv_a = FeatureView( name="driver_stats", From 6a65ce7d80a3259d852f33aff266b86606c639b0 Mon Sep 17 00:00:00 2001 From: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> Date: Tue, 5 May 2026 23:01:42 +0100 Subject: [PATCH 4/6] test: use prep_file_source to create a valid parquet file in cross-project test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apply() reads the parquet schema when the apply succeeds (no early exception). Use the prep_file_source() helper — the same pattern all other tests that call apply() use — to produce a real, schema-valid parquet file instead of an empty temp file. Signed-off-by: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> --- .../test_local_feature_store.py | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py index f54b73897f4..b7341a267a2 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py +++ b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py @@ -547,7 +547,6 @@ def test_cross_project_feature_view_names_do_not_conflict(): """Feature views with the same name in different projects must not raise ConflictingFeatureViewNames.""" fd, registry_path = mkstemp() fd, online_store_path = mkstemp() - fd, parquet_path = mkstemp(suffix=".parquet") def make_store(project: str) -> FeatureStore: return FeatureStore( @@ -564,28 +563,34 @@ def make_store(project: str) -> FeatureStore: store_b = make_store("project_b") entity = Entity(name="driver", join_keys=["driver_id"]) - source = FileSource(path=parquet_path) - - fv_a = FeatureView( - name="driver_stats", - entities=[entity], - schema=[Field(name="driver_id", dtype=Int64)], - ttl=timedelta(seconds=10), - online=False, - source=source, + df = pd.DataFrame( + { + "driver_id": [1, 2], + "ts": pd.to_datetime(["2024-01-01", "2024-01-02"], utc=True), + } ) - store_a.apply([entity, fv_a]) - fv_b = FeatureView( - name="driver_stats", - entities=[entity], - schema=[Field(name="driver_id", dtype=Int64)], - ttl=timedelta(seconds=10), - online=False, - source=source, - ) - # Must not raise ConflictingFeatureViewNames — same name but different project. - store_b.apply([entity, fv_b]) + with prep_file_source(df=df, timestamp_field="ts") as source: + fv_a = FeatureView( + name="driver_stats", + entities=[entity], + schema=[Field(name="driver_id", dtype=Int64)], + ttl=timedelta(seconds=10), + online=False, + source=source, + ) + store_a.apply([entity, fv_a]) + + fv_b = FeatureView( + name="driver_stats", + entities=[entity], + schema=[Field(name="driver_id", dtype=Int64)], + ttl=timedelta(seconds=10), + online=False, + source=source, + ) + # Must not raise ConflictingFeatureViewNames — same name but different project. + store_b.apply([entity, fv_b]) store_a.teardown() store_b.teardown() From cb577e3eac133d6fb2e18f70888f468838adb43d Mon Sep 17 00:00:00 2001 From: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> Date: Tue, 5 May 2026 23:25:17 +0100 Subject: [PATCH 5/6] test: add registry-level cross-project name conflict regression test Replace the FeatureStore-level test (which fought parquet inference and dill source extraction) with a direct Registry-level test mirroring the pattern in test_sql_registry.py. Two tests in tests/unit/infra/registry/test_file_registry.py: - test_same_project_name_conflict_batch_vs_stream: confirms that a FeatureView and StreamFeatureView with the same name in the same project still raise ConflictingFeatureViewNames (existing behaviour) - test_cross_project_name_does_not_conflict_batch_vs_stream: confirms that the same name across different projects and different view types no longer raises (the bug fixed by this PR) Signed-off-by: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> --- .../unit/infra/registry/test_file_registry.py | 94 +++++++++++++++++++ .../test_local_feature_store.py | 53 ----------- 2 files changed, 94 insertions(+), 53 deletions(-) create mode 100644 sdk/python/tests/unit/infra/registry/test_file_registry.py diff --git a/sdk/python/tests/unit/infra/registry/test_file_registry.py b/sdk/python/tests/unit/infra/registry/test_file_registry.py new file mode 100644 index 00000000000..256ff1e22be --- /dev/null +++ b/sdk/python/tests/unit/infra/registry/test_file_registry.py @@ -0,0 +1,94 @@ +import tempfile +from datetime import timedelta + +import pytest + +from feast import Field +from feast.data_source import PushSource +from feast.entity import Entity +from feast.errors import ConflictingFeatureViewNames +from feast.feature_view import FeatureView +from feast.infra.offline_stores.file_source import FileSource +from feast.infra.registry.registry import Registry +from feast.repo_config import RegistryConfig +from feast.stream_feature_view import StreamFeatureView +from feast.types import Float32 +from feast.value_type import ValueType + + +@pytest.fixture +def file_registry(): + fd, registry_path = tempfile.mkstemp() + config = RegistryConfig(path=registry_path) + registry = Registry("test_project", config, None) + yield registry + + +def _make_sources(): + file_source = FileSource( + path="driver_stats.parquet", + timestamp_field="event_timestamp", + created_timestamp_column="created", + ) + push_source = PushSource(name="driver_push", batch_source=file_source) + return file_source, push_source + + +def test_same_project_name_conflict_batch_vs_stream(file_registry): + """A FeatureView and StreamFeatureView with the same name in the same project must raise ConflictingFeatureViewNames.""" + entity = Entity(name="driver", value_type=ValueType.STRING, join_keys=["driver_id"]) + file_registry.apply_entity(entity, "test_project") + + file_source, push_source = _make_sources() + + batch_view = FeatureView( + name="driver_activity", + entities=[entity], + ttl=timedelta(days=1), + schema=[Field(name="conv_rate", dtype=Float32)], + source=file_source, + ) + file_registry.apply_feature_view(batch_view, "test_project") + + stream_view = StreamFeatureView( + name="driver_activity", + source=push_source, + entities=[entity], + schema=[Field(name="conv_rate", dtype=Float32)], + timestamp_field="event_timestamp", + ) + with pytest.raises(ConflictingFeatureViewNames): + file_registry.apply_feature_view(stream_view, "test_project") + + +def test_cross_project_name_does_not_conflict_batch_vs_stream(file_registry): + """A FeatureView in project_a and a StreamFeatureView with the same name in project_b + must not raise ConflictingFeatureViewNames. + + Before the fix, _existing_feature_view_names_to_fvs scanned all projects, + so the type mismatch between the two projects triggered a spurious error. + """ + entity = Entity(name="driver", value_type=ValueType.STRING, join_keys=["driver_id"]) + file_registry.apply_entity(entity, "project_a") + file_registry.apply_entity(entity, "project_b") + + file_source, push_source = _make_sources() + + batch_view = FeatureView( + name="driver_activity", + entities=[entity], + ttl=timedelta(days=1), + schema=[Field(name="conv_rate", dtype=Float32)], + source=file_source, + ) + file_registry.apply_feature_view(batch_view, "project_a") + + stream_view = StreamFeatureView( + name="driver_activity", + source=push_source, + entities=[entity], + schema=[Field(name="conv_rate", dtype=Float32)], + timestamp_field="event_timestamp", + ) + # Must not raise — same name, different project, different type. + file_registry.apply_feature_view(stream_view, "project_b") diff --git a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py index b7341a267a2..9b7660bf692 100644 --- a/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py +++ b/sdk/python/tests/unit/local_feast_tests/test_local_feature_store.py @@ -543,59 +543,6 @@ def test_apply_conflicting_feature_view_names(feature_store_with_local_registry) feature_store_with_local_registry.teardown() -def test_cross_project_feature_view_names_do_not_conflict(): - """Feature views with the same name in different projects must not raise ConflictingFeatureViewNames.""" - fd, registry_path = mkstemp() - fd, online_store_path = mkstemp() - - def make_store(project: str) -> FeatureStore: - return FeatureStore( - config=RepoConfig( - registry=registry_path, - project=project, - provider="local", - online_store=SqliteOnlineStoreConfig(path=online_store_path), - entity_key_serialization_version=3, - ) - ) - - store_a = make_store("project_a") - store_b = make_store("project_b") - - entity = Entity(name="driver", join_keys=["driver_id"]) - df = pd.DataFrame( - { - "driver_id": [1, 2], - "ts": pd.to_datetime(["2024-01-01", "2024-01-02"], utc=True), - } - ) - - with prep_file_source(df=df, timestamp_field="ts") as source: - fv_a = FeatureView( - name="driver_stats", - entities=[entity], - schema=[Field(name="driver_id", dtype=Int64)], - ttl=timedelta(seconds=10), - online=False, - source=source, - ) - store_a.apply([entity, fv_a]) - - fv_b = FeatureView( - name="driver_stats", - entities=[entity], - schema=[Field(name="driver_id", dtype=Int64)], - ttl=timedelta(seconds=10), - online=False, - source=source, - ) - # Must not raise ConflictingFeatureViewNames — same name but different project. - store_b.apply([entity, fv_b]) - - store_a.teardown() - store_b.teardown() - - @pytest.mark.parametrize( "test_feature_store", [lazy_fixture("feature_store_with_local_registry")], From e9b2e8178a3d006e7370c1465c8e4e658c65ebf6 Mon Sep 17 00:00:00 2001 From: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> Date: Tue, 5 May 2026 23:27:16 +0100 Subject: [PATCH 6/6] test: add teardown to file_registry fixture to match sql test style Signed-off-by: Abhishek8108 <87538407+Abhishek8108@users.noreply.github.com> --- sdk/python/tests/unit/infra/registry/test_file_registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/python/tests/unit/infra/registry/test_file_registry.py b/sdk/python/tests/unit/infra/registry/test_file_registry.py index 256ff1e22be..07b443e7eaa 100644 --- a/sdk/python/tests/unit/infra/registry/test_file_registry.py +++ b/sdk/python/tests/unit/infra/registry/test_file_registry.py @@ -22,6 +22,7 @@ def file_registry(): config = RegistryConfig(path=registry_path) registry = Registry("test_project", config, None) yield registry + registry.teardown() def _make_sources():