@@ -840,3 +840,102 @@ def test_historical_features_non_entity_retrieval(environment):
840840 assert 300 in actual_trips , (
841841 "Latest trip value 300 for driver 1002 should be present"
842842 )
843+
844+
845+ @pytest .mark .integration
846+ @pytest .mark .universal_offline_stores
847+ @pytest .mark .parametrize ("full_feature_names" , [True , False ], ids = lambda v : f"full:{ v } " )
848+ def test_odfv_projection (environment , universal_data_sources , full_feature_names ):
849+ """
850+ Test that requesting a subset of ODFV features only returns those features.
851+
852+ Regression test for issue #6099: OnDemandFeatureViews should honor output
853+ projection in offline retrieval, matching the behavior of online retrieval.
854+
855+ Before the fix, offline retrieval would return ALL ODFV output features even
856+ when only a subset was requested, while online retrieval correctly returned
857+ only the requested features.
858+ """
859+ store = environment .feature_store
860+
861+ (entities , datasets , data_sources ) = universal_data_sources
862+
863+ feature_views = construct_universal_feature_views (data_sources )
864+
865+ # Add request data needed for ODFV
866+ entity_df_with_request_data = datasets .entity_df .copy (deep = True )
867+ entity_df_with_request_data ["val_to_add" ] = [
868+ i for i in range (len (entity_df_with_request_data ))
869+ ]
870+
871+ store .apply ([driver (), * feature_views .values ()])
872+
873+ # The conv_rate_plus_100 ODFV has 3 output features:
874+ # - conv_rate_plus_100
875+ # - conv_rate_plus_val_to_add
876+ # - conv_rate_plus_100_rounded
877+
878+ # Test 1: Request only ONE ODFV feature
879+ job = store .get_historical_features (
880+ entity_df = entity_df_with_request_data ,
881+ features = [
882+ "conv_rate_plus_100:conv_rate_plus_100" , # Request only this one
883+ ],
884+ full_feature_names = full_feature_names ,
885+ )
886+
887+ actual_df = job .to_df ()
888+
889+ # Determine expected column names based on full_feature_names setting
890+ expected_feature = (
891+ "conv_rate_plus_100__conv_rate_plus_100"
892+ if full_feature_names
893+ else "conv_rate_plus_100"
894+ )
895+ unrequested_feature_1 = (
896+ "conv_rate_plus_100__conv_rate_plus_val_to_add"
897+ if full_feature_names
898+ else "conv_rate_plus_val_to_add"
899+ )
900+ unrequested_feature_2 = (
901+ "conv_rate_plus_100__conv_rate_plus_100_rounded"
902+ if full_feature_names
903+ else "conv_rate_plus_100_rounded"
904+ )
905+
906+ # Verify the requested feature is present
907+ assert expected_feature in actual_df .columns , (
908+ f"Requested feature '{ expected_feature } ' should be in the result"
909+ )
910+
911+ # Verify unrequested ODFV features are NOT present (this is the key fix)
912+ assert unrequested_feature_1 not in actual_df .columns , (
913+ f"Unrequested ODFV feature '{ unrequested_feature_1 } ' should NOT be in the result. "
914+ f"This indicates the bug from issue #6099 still exists."
915+ )
916+ assert unrequested_feature_2 not in actual_df .columns , (
917+ f"Unrequested ODFV feature '{ unrequested_feature_2 } ' should NOT be in the result. "
918+ f"This indicates the bug from issue #6099 still exists."
919+ )
920+
921+ # Test 2: Request TWO out of THREE ODFV features
922+ job2 = store .get_historical_features (
923+ entity_df = entity_df_with_request_data ,
924+ features = [
925+ "conv_rate_plus_100:conv_rate_plus_100" ,
926+ "conv_rate_plus_100:conv_rate_plus_val_to_add" ,
927+ # Deliberately NOT requesting conv_rate_plus_100_rounded
928+ ],
929+ full_feature_names = full_feature_names ,
930+ )
931+
932+ actual_df2 = job2 .to_df ()
933+
934+ # Verify the two requested features are present
935+ assert expected_feature in actual_df2 .columns
936+ assert unrequested_feature_1 in actual_df2 .columns
937+
938+ # Verify the unrequested feature is NOT present
939+ assert unrequested_feature_2 not in actual_df2 .columns , (
940+ f"Unrequested ODFV feature '{ unrequested_feature_2 } ' should NOT be in the result"
941+ )
0 commit comments