diff --git a/sdk/python/feast/utils.py b/sdk/python/feast/utils.py index 831ed622d06..4f8d18ad080 100644 --- a/sdk/python/feast/utils.py +++ b/sdk/python/feast/utils.py @@ -1568,6 +1568,13 @@ def _prepare_entities_to_read_from_online_store( entityless_case, ) = _get_cached_request_context(registry, project, features, full_feature_names) + # Online stores may append internal fields to the requested feature lists. + # Keep those request-local so the cached resolution result remains unchanged. + grouped_refs = [ + (feature_view, list(requested_features)) + for feature_view, requested_features in grouped_refs + ] + # Mutable copy — downstream code adds join keys to this set. requested_result_row_names = set(requested_result_row_names_frozen) diff --git a/sdk/python/tests/unit/test_feature_resolution_cache.py b/sdk/python/tests/unit/test_feature_resolution_cache.py index f77a4c1e0ac..f8d2929dd85 100644 --- a/sdk/python/tests/unit/test_feature_resolution_cache.py +++ b/sdk/python/tests/unit/test_feature_resolution_cache.py @@ -6,6 +6,7 @@ import pytest import feast.utils as utils +from feast.protos.feast.types.Value_pb2 import Value as ValueProto @pytest.fixture(autouse=True) @@ -140,6 +141,36 @@ def test_returned_result_row_names_is_mutable_copy(self, mock_ctx): assert isinstance(result_row_names_1, frozenset) assert result_row_names_1 == result_row_names_2 + @patch("feast.utils._get_online_request_context") + def test_grouped_refs_are_copied_per_request(self, mock_ctx): + """Downstream mutations must not leak into cached grouped refs.""" + mock_ctx.return_value = _make_context() + registry = _make_registry(datetime.now(tz=timezone.utc)) + entity_values = {"user_id": [ValueProto(int64_val=1)]} + + first_result = utils._prepare_entities_to_read_from_online_store( + registry, + "proj", + ["fv:feat1"], + entity_values, + native_entity_values=False, + ) + first_requested_features = first_result[1][0][1] + first_requested_features.append("_ts:fv") + + second_result = utils._prepare_entities_to_read_from_online_store( + registry, + "proj", + ["fv:feat1"], + entity_values, + native_entity_values=False, + ) + second_requested_features = second_result[1][0][1] ++ assert first_requested_features == ["feat1", "_ts:fv"] ++ assert second_requested_features == ["feat1"] ++ assert second_requested_features is not first_requested_features ++ mock_ctx.assert_called_once() + @patch("feast.utils._get_online_request_context") def test_feature_service_cached_separately(self, mock_ctx): """FeatureService objects should be cached by name."""