From 713da8c145eb13b7bfd27442c9cf4659a9f31bd3 Mon Sep 17 00:00:00 2001 From: Linda Oraegbunam Date: Thu, 11 Jun 2026 20:32:28 +0100 Subject: [PATCH 1/2] docs: add end-to-end registry deletion lifecycle snippet (#5360) The existing registry deletion docs cover the CLI and individual Python SDK delete methods, but lack a single copy-pasteable example showing the full create -> verify -> delete -> confirm flow. - Add an 'End-to-end example' snippet to registry.md that lists a feature view, deletes it with delete_feature_view(), and lists again to confirm. - Add a unit test for FeatureView deletion via apply(objects_to_delete=..., partial=False) to guard the programmatic deletion path. Signed-off-by: Linda Oraegbunam --- docs/getting-started/components/registry.md | 24 ++++++++ .../test_local_feature_store.py | 60 ++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/components/registry.md b/docs/getting-started/components/registry.md index 9723e6cfe63..34beae25084 100644 --- a/docs/getting-started/components/registry.md +++ b/docs/getting-started/components/registry.md @@ -69,6 +69,30 @@ store._registry.delete_validation_reference("my_validation_reference", project=s When using `feast apply` via the CLI, you can also use the `objects_to_delete` parameter with `partial=False` to delete objects as part of the apply operation. However, this is less common and typically used in automated deployment scenarios. {% endhint %} +### End-to-end example + +The following snippet shows the full lifecycle of deleting a feature view from the registry: + +```python +from feast import FeatureStore + +store = FeatureStore(repo_path=".") + +# 1. Verify the object exists before deletion +print(store.list_batch_feature_views()) # shows my_feature_view + +# 2. Delete the feature view +store.delete_feature_view("my_feature_view") + +# 3. Confirm it's gone +print(store.list_batch_feature_views()) # my_feature_view no longer listed + +# Trying to fetch it now raises FeatureViewNotFoundException +# store.get_feature_view("my_feature_view") +``` + +The same pattern works for other registry objects: list/verify the object, call the corresponding `delete_*` method, then list again to confirm the deletion. + ## Accessing the registry from clients Users can specify the registry through a `feature_store.yaml` config file, or programmatically. We often see teams 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 ec2513f0726..73dbb2addbf 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 @@ -11,7 +11,7 @@ from feast.data_format import AvroFormat, ParquetFormat from feast.data_source import KafkaSource from feast.entity import Entity -from feast.errors import ConflictingFeatureViewNames +from feast.errors import ConflictingFeatureViewNames, FeatureViewNotFoundException from feast.feast_object import ALL_RESOURCE_TYPES from feast.feature_store import FeatureStore from feast.feature_view import DUMMY_ENTITY_ID, DUMMY_ENTITY_NAME, FeatureView @@ -443,6 +443,64 @@ def test_apply_permissions(test_feature_store): test_feature_store.teardown() +@pytest.mark.parametrize( + "test_feature_store", + [lazy_fixture("feature_store_with_local_registry")], +) +def test_apply_delete_feature_view(test_feature_store): + """Test that a feature view can be deleted using objects_to_delete with partial=False.""" + assert isinstance(test_feature_store, FeatureStore) + + now = pd.Timestamp.utcnow().round("ms") + dataframe_source = pd.DataFrame( + { + "test_key": [1, 2, 1, 3, 3], + "feature_value": [0.1, 0.2, 0.3, 4.0, 5.0], + "ts_1": [ + now, + now - pd.Timedelta(hours=4), + now - pd.Timedelta(hours=3), + now - pd.Timedelta(hours=2), + now - pd.Timedelta(hours=1), + ], + } + ) + + with prep_file_source(df=dataframe_source, timestamp_field="ts_1") as file_source: + entity = Entity( + name="driver_entity", join_keys=["test_key"], value_type=ValueType.INT64 + ) + driver_fv = FeatureView( + name="driver_fv_to_delete", + entities=[entity], + schema=[Field(name="test_key", dtype=Int64)], + source=file_source, + ) + + # Register entity and feature view + test_feature_store.apply([entity, driver_fv]) + + # Verify feature view exists + fvs = test_feature_store.list_batch_feature_views() + assert len(fvs) == 1 + assert fvs[0].name == driver_fv.name + + # Delete the feature view using objects_to_delete + test_feature_store.apply( + objects=[], objects_to_delete=[driver_fv], partial=False + ) + + # Verify feature view is deleted + fvs = test_feature_store.list_batch_feature_views() + assert len(fvs) == 0 + + # Verify get_feature_view raises FeatureViewNotFoundException + with pytest.raises(FeatureViewNotFoundException): + test_feature_store.get_feature_view(driver_fv.name) + + test_feature_store.teardown() + + @pytest.mark.parametrize( "test_feature_store", [lazy_fixture("feature_store_with_local_registry")], From 92523ec38002db99eb56defb9937770fd2f05da2 Mon Sep 17 00:00:00 2001 From: Linda Oraegbunam Date: Tue, 18 Aug 2026 19:45:15 +0100 Subject: [PATCH 2/2] test: cover delete_feature_view, the API the docs example uses Addresses review feedback on #6504: the new docs snippet demonstrates store.delete_feature_view(name), but the only test added went through apply(objects_to_delete=..., partial=False), so the documented API was still untested. - Add test_delete_feature_view, mirroring the registry.md snippet step for step: list, delete by name, list again, then assert get_feature_view raises FeatureViewNotFoundException. - Add test_delete_feature_view_raises_when_missing, covering the FeatureViewNotFoundException that delete_feature_view's own docstring promises for an unregistered name. - Keep the apply(objects_to_delete=...) test for the `feast apply` path documented in the hint block, and note in its docstring that it is deliberately distinct from delete_feature_view. - Lift the shared source frame and registration into two helpers so the two lifecycle tests do not duplicate ~30 lines of setup. Signed-off-by: Linda Oraegbunam --- .../test_local_feature_store.py | 109 +++++++++++++----- 1 file changed, 83 insertions(+), 26 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 73dbb2addbf..63f51bcef6e 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 @@ -443,16 +443,30 @@ def test_apply_permissions(test_feature_store): test_feature_store.teardown() -@pytest.mark.parametrize( - "test_feature_store", - [lazy_fixture("feature_store_with_local_registry")], -) -def test_apply_delete_feature_view(test_feature_store): - """Test that a feature view can be deleted using objects_to_delete with partial=False.""" - assert isinstance(test_feature_store, FeatureStore) +def _apply_feature_view_to_delete(test_feature_store, file_source): + """Register an entity and a feature view, and return the feature view.""" + entity = Entity( + name="driver_entity", join_keys=["test_key"], value_type=ValueType.INT64 + ) + driver_fv = FeatureView( + name="driver_fv_to_delete", + entities=[entity], + schema=[Field(name="test_key", dtype=Int64)], + source=file_source, + ) + test_feature_store.apply([entity, driver_fv]) + + fvs = test_feature_store.list_batch_feature_views() + assert len(fvs) == 1 + assert fvs[0].name == driver_fv.name + + return driver_fv + +def _deletion_source_dataframe(): + """Build the small source frame both deletion tests register against.""" now = pd.Timestamp.utcnow().round("ms") - dataframe_source = pd.DataFrame( + return pd.DataFrame( { "test_key": [1, 2, 1, 3, 3], "feature_value": [0.1, 0.2, 0.3, 4.0, 5.0], @@ -466,24 +480,68 @@ def test_apply_delete_feature_view(test_feature_store): } ) - with prep_file_source(df=dataframe_source, timestamp_field="ts_1") as file_source: - entity = Entity( - name="driver_entity", join_keys=["test_key"], value_type=ValueType.INT64 - ) - driver_fv = FeatureView( - name="driver_fv_to_delete", - entities=[entity], - schema=[Field(name="test_key", dtype=Int64)], - source=file_source, - ) - # Register entity and feature view - test_feature_store.apply([entity, driver_fv]) +@pytest.mark.parametrize( + "test_feature_store", + [lazy_fixture("feature_store_with_local_registry")], +) +def test_delete_feature_view(test_feature_store): + """Test the delete_feature_view lifecycle documented in registry.md. - # Verify feature view exists - fvs = test_feature_store.list_batch_feature_views() - assert len(fvs) == 1 - assert fvs[0].name == driver_fv.name + Mirrors the end-to-end snippet in docs/getting-started/components/registry.md: + list the object, delete it by name, list again to confirm it is gone, and + check that fetching it afterwards raises FeatureViewNotFoundException. + """ + assert isinstance(test_feature_store, FeatureStore) + + with prep_file_source( + df=_deletion_source_dataframe(), timestamp_field="ts_1" + ) as file_source: + driver_fv = _apply_feature_view_to_delete(test_feature_store, file_source) + + # Delete the feature view by name + test_feature_store.delete_feature_view(driver_fv.name) + + # Verify feature view is deleted + assert len(test_feature_store.list_batch_feature_views()) == 0 + + # Verify get_feature_view raises FeatureViewNotFoundException + with pytest.raises(FeatureViewNotFoundException): + test_feature_store.get_feature_view(driver_fv.name) + + test_feature_store.teardown() + + +@pytest.mark.parametrize( + "test_feature_store", + [lazy_fixture("feature_store_with_local_registry")], +) +def test_delete_feature_view_raises_when_missing(test_feature_store): + """Deleting a feature view that was never registered raises, as documented.""" + assert isinstance(test_feature_store, FeatureStore) + + with pytest.raises(FeatureViewNotFoundException): + test_feature_store.delete_feature_view("feature_view_that_does_not_exist") + + test_feature_store.teardown() + + +@pytest.mark.parametrize( + "test_feature_store", + [lazy_fixture("feature_store_with_local_registry")], +) +def test_apply_delete_feature_view(test_feature_store): + """Test that a feature view can be deleted using objects_to_delete with partial=False. + + This is the `feast apply` path called out in the hint block in registry.md, + and is distinct from the delete_feature_view path covered above. + """ + assert isinstance(test_feature_store, FeatureStore) + + with prep_file_source( + df=_deletion_source_dataframe(), timestamp_field="ts_1" + ) as file_source: + driver_fv = _apply_feature_view_to_delete(test_feature_store, file_source) # Delete the feature view using objects_to_delete test_feature_store.apply( @@ -491,8 +549,7 @@ def test_apply_delete_feature_view(test_feature_store): ) # Verify feature view is deleted - fvs = test_feature_store.list_batch_feature_views() - assert len(fvs) == 0 + assert len(test_feature_store.list_batch_feature_views()) == 0 # Verify get_feature_view raises FeatureViewNotFoundException with pytest.raises(FeatureViewNotFoundException):