@@ -162,6 +162,65 @@ def test_from_response_json_transformation_function(self, mocker, backend_fixtur
162162 assert len (fv .schema ) == 2
163163 assert isinstance (fv .schema [0 ], training_dataset_feature .TrainingDatasetFeature )
164164
165+ def test_on_demand_transformation_functions_dedup_multi_output_tf (self , mocker ):
166+ """Regression: a multi-output ODT is attached to each feature it produces.
167+
168+ Without dedup, the property returns the TF twice — once per feature — and
169+ TransformationExecutionDAG raises "Output column 'x' is produced by both
170+ 'tf' and 'tf'." This was hit at FV creation time when reading the
171+ backend response back, breaking any chained pipeline with a multi-output
172+ TF (e.g. add_two -> odt2_1, odt2_2).
173+ """
174+ from hsfs .core import transformation_execution_dag
175+ from hsfs .transformation_function import (
176+ TransformationFunction ,
177+ TransformationType ,
178+ )
179+
180+ mocker .patch ("hopsworks_common.client.get_instance" )
181+ mocker .patch ("hsfs.engine.get_type" , return_value = "python" )
182+
183+ @udf ([float , float ])
184+ def add_two (feature ):
185+ return feature + 2 , feature + 2
186+
187+ tf = TransformationFunction (
188+ featurestore_id = 99 ,
189+ hopsworks_udf = add_two ,
190+ transformation_type = TransformationType .ON_DEMAND ,
191+ )("feature" ).alias ("odt2_1" , "odt2_2" )
192+
193+ fv = feature_view .FeatureView (
194+ name = "fv_dedup" ,
195+ query = fg1 .select_features (),
196+ featurestore_id = 99 ,
197+ featurestore_name = "test_fs" ,
198+ )
199+ # Multi-output TF -> the SAME TF appears on every output feature.
200+ # The backend response path produces fresh Python instances per feature,
201+ # so simulate that by attaching distinct copies that compare equal.
202+ fv .schema = [
203+ training_dataset_feature .TrainingDatasetFeature (
204+ name = "odt2_1" ,
205+ type = "float" ,
206+ transformation_function = tf ,
207+ ),
208+ training_dataset_feature .TrainingDatasetFeature (
209+ name = "odt2_2" ,
210+ type = "float" ,
211+ transformation_function = tf ,
212+ ),
213+ ]
214+
215+ # Property must dedup
216+ odts = fv ._on_demand_transformation_functions
217+ assert len (odts ) == 1
218+ assert odts [0 ].hopsworks_udf .function_name == "add_two"
219+
220+ # DAG construction must succeed (no "produced by both 'add_two' and 'add_two'")
221+ dag = transformation_execution_dag .TransformationExecutionDAG (odts )
222+ assert len (dag .nodes ) == 1
223+
165224 def test_from_response_json_basic_info_deprecated (self , mocker , backend_fixtures ):
166225 # Arrange
167226 mocker .patch ("hsfs.engine.get_type" )
0 commit comments