6868 TransformWithStateInPySparkRowSerializer ,
6969 TransformWithStateInPySparkRowInitStateSerializer ,
7070 ArrowStreamAggPandasUDFSerializer ,
71- ArrowStreamAggArrowUDFSerializer ,
7271 ArrowBatchUDFSerializer ,
7372 ArrowStreamUDTFSerializer ,
7473 ArrowStreamArrowUDTFSerializer ,
@@ -1089,26 +1088,6 @@ def wrap_window_agg_pandas_udf(
10891088 )
10901089
10911090
1092- def wrap_window_agg_arrow_udf (f , args_offsets , kwargs_offsets , return_type , runner_conf , udf_index ):
1093- window_bound_types_str = runner_conf .get ("window_bound_types" )
1094- window_bound_type = [t .strip ().lower () for t in window_bound_types_str .split ("," )][udf_index ]
1095- if window_bound_type == "bounded" :
1096- return wrap_bounded_window_agg_arrow_udf (
1097- f , args_offsets , kwargs_offsets , return_type , runner_conf
1098- )
1099- elif window_bound_type == "unbounded" :
1100- return wrap_unbounded_window_agg_arrow_udf (
1101- f , args_offsets , kwargs_offsets , return_type , runner_conf
1102- )
1103- else :
1104- raise PySparkRuntimeError (
1105- errorClass = "INVALID_WINDOW_BOUND_TYPE" ,
1106- messageParameters = {
1107- "window_bound_type" : window_bound_type ,
1108- },
1109- )
1110-
1111-
11121091def wrap_unbounded_window_agg_pandas_udf (f , args_offsets , kwargs_offsets , return_type , runner_conf ):
11131092 func , args_kwargs_offsets = wrap_kwargs_support (f , args_offsets , kwargs_offsets )
11141093
@@ -1128,27 +1107,6 @@ def wrapped(*series):
11281107 )
11291108
11301109
1131- def wrap_unbounded_window_agg_arrow_udf (f , args_offsets , kwargs_offsets , return_type , runner_conf ):
1132- func , args_kwargs_offsets = wrap_kwargs_support (f , args_offsets , kwargs_offsets )
1133-
1134- # This is similar to wrap_unbounded_window_agg_pandas_udf, the only difference
1135- # is that this function is for arrow udf.
1136- arrow_return_type = to_arrow_type (
1137- return_type , timezone = "UTC" , prefers_large_types = runner_conf .use_large_var_types
1138- )
1139-
1140- def wrapped (* series ):
1141- import pyarrow as pa
1142-
1143- result = func (* series )
1144- return pa .repeat (result , len (series [0 ]))
1145-
1146- return (
1147- args_kwargs_offsets ,
1148- lambda * a : (wrapped (* a ), arrow_return_type ),
1149- )
1150-
1151-
11521110def wrap_bounded_window_agg_pandas_udf (f , args_offsets , kwargs_offsets , return_type , runner_conf ):
11531111 # args_offsets should have at least 2 for begin_index, end_index.
11541112 assert len (args_offsets ) >= 2 , len (args_offsets )
@@ -1189,35 +1147,6 @@ def wrapped(begin_index, end_index, *series):
11891147 )
11901148
11911149
1192- def wrap_bounded_window_agg_arrow_udf (f , args_offsets , kwargs_offsets , return_type , runner_conf ):
1193- # args_offsets should have at least 2 for begin_index, end_index.
1194- assert len (args_offsets ) >= 2 , len (args_offsets )
1195- func , args_kwargs_offsets = wrap_kwargs_support (f , args_offsets [2 :], kwargs_offsets )
1196-
1197- arrow_return_type = to_arrow_type (
1198- return_type , timezone = "UTC" , prefers_large_types = runner_conf .use_large_var_types
1199- )
1200-
1201- def wrapped (begin_index , end_index , * series ):
1202- import pyarrow as pa
1203-
1204- assert isinstance (begin_index , pa .Int32Array ), type (begin_index )
1205- assert isinstance (end_index , pa .Int32Array ), type (end_index )
1206-
1207- result = []
1208- for i in range (len (begin_index )):
1209- offset = begin_index [i ].as_py ()
1210- length = end_index [i ].as_py () - offset
1211- series_slices = [s .slice (offset = offset , length = length ) for s in series ]
1212- result .append (func (* series_slices ))
1213- return pa .array (result )
1214-
1215- return (
1216- args_offsets [:2 ] + args_kwargs_offsets ,
1217- lambda * a : (wrapped (* a ), arrow_return_type ),
1218- )
1219-
1220-
12211150def wrap_kwargs_support (f , args_offsets , kwargs_offsets ):
12221151 if len (kwargs_offsets ):
12231152 keys = list (kwargs_offsets .keys ())
@@ -1432,9 +1361,7 @@ def read_single_udf(pickleSer, infile, eval_type, runner_conf, udf_index):
14321361 func , args_offsets , kwargs_offsets , return_type , runner_conf , udf_index
14331362 )
14341363 elif eval_type == PythonEvalType .SQL_WINDOW_AGG_ARROW_UDF :
1435- return wrap_window_agg_arrow_udf (
1436- func , args_offsets , kwargs_offsets , return_type , runner_conf , udf_index
1437- )
1364+ return func , args_offsets , kwargs_offsets , return_type
14381365 elif eval_type == PythonEvalType .SQL_BATCHED_UDF :
14391366 return wrap_udf (func , args_offsets , kwargs_offsets , return_type )
14401367 else :
@@ -2648,7 +2575,7 @@ def read_udfs(pickleSer, infile, eval_type, runner_conf, eval_conf):
26482575 ):
26492576 ser = ArrowStreamGroupSerializer (write_start_stream = True )
26502577 elif eval_type == PythonEvalType .SQL_WINDOW_AGG_ARROW_UDF :
2651- ser = ArrowStreamAggArrowUDFSerializer ( safecheck = True , arrow_cast = True )
2578+ ser = ArrowStreamGroupSerializer ( write_start_stream = True )
26522579 elif eval_type in (
26532580 PythonEvalType .SQL_GROUPED_AGG_PANDAS_UDF ,
26542581 PythonEvalType .SQL_GROUPED_AGG_PANDAS_ITER_UDF ,
@@ -2969,6 +2896,72 @@ def func(split_index: int, batches: Iterator[Any]) -> Iterator[pa.RecordBatch]:
29692896 # profiling is not supported for UDF
29702897 return func , None , ser , ser
29712898
2899+ if eval_type == PythonEvalType .SQL_WINDOW_AGG_ARROW_UDF :
2900+ import pyarrow as pa
2901+
2902+ window_bound_types_str = runner_conf .get ("window_bound_types" )
2903+ window_bound_types = [t .strip ().lower () for t in window_bound_types_str .split ("," )]
2904+
2905+ col_names = ["_%d" % i for i in range (len (udfs ))]
2906+ return_schema = to_arrow_schema (
2907+ StructType ([StructField (name , rt ) for name , (_ , _ , _ , rt ) in zip (col_names , udfs )]),
2908+ timezone = "UTC" ,
2909+ prefers_large_types = runner_conf .use_large_var_types ,
2910+ )
2911+
2912+ def func (split_index : int , batches : Iterator [Any ]) -> Iterator [pa .RecordBatch ]:
2913+ for group_batches in batches :
2914+ batch_list = list (group_batches )
2915+ if not batch_list :
2916+ continue
2917+ if hasattr (pa , "concat_batches" ):
2918+ concatenated = pa .concat_batches (batch_list )
2919+ else :
2920+ # pyarrow.concat_batches not supported before 19.0.0
2921+ # remove this once we drop support for old versions
2922+ concatenated = pa .RecordBatch .from_struct_array (
2923+ pa .concat_arrays ([b .to_struct_array () for b in batch_list ])
2924+ )
2925+ num_rows = concatenated .num_rows
2926+
2927+ result_arrays = []
2928+ for udf_index , (udf_func , args_offsets , kwargs_offsets , _ ) in enumerate (udfs ):
2929+ bound_type = window_bound_types [udf_index ]
2930+ if bound_type == "unbounded" :
2931+ result = udf_func (
2932+ * [concatenated .column (o ) for o in args_offsets ],
2933+ ** {k : concatenated .column (v ) for k , v in kwargs_offsets .items ()},
2934+ )
2935+ result_arrays .append (pa .repeat (result , num_rows ))
2936+ elif bound_type == "bounded" :
2937+ begin_col = concatenated .column (args_offsets [0 ])
2938+ end_col = concatenated .column (args_offsets [1 ])
2939+ results = []
2940+ for i in range (num_rows ):
2941+ offset = begin_col [i ].as_py ()
2942+ length = end_col [i ].as_py () - offset
2943+ slices = [
2944+ concatenated .column (o ).slice (offset = offset , length = length )
2945+ for o in args_offsets [2 :]
2946+ ]
2947+ kw_slices = {
2948+ k : concatenated .column (v ).slice (offset = offset , length = length )
2949+ for k , v in kwargs_offsets .items ()
2950+ }
2951+ results .append (udf_func (* slices , ** kw_slices ))
2952+ result_arrays .append (pa .array (results ))
2953+ else :
2954+ raise PySparkRuntimeError (
2955+ errorClass = "INVALID_WINDOW_BOUND_TYPE" ,
2956+ messageParameters = {"window_bound_type" : bound_type },
2957+ )
2958+
2959+ batch = pa .RecordBatch .from_arrays (result_arrays , col_names )
2960+ yield ArrowBatchTransformer .enforce_schema (batch , return_schema )
2961+
2962+ # profiling is not supported for UDF
2963+ return func , None , ser , ser
2964+
29722965 is_scalar_iter = eval_type == PythonEvalType .SQL_SCALAR_PANDAS_ITER_UDF
29732966 is_map_pandas_iter = eval_type == PythonEvalType .SQL_MAP_PANDAS_ITER_UDF
29742967
@@ -3393,42 +3386,6 @@ def mapper(batch_iter):
33933386 )
33943387 return f (series_iter )
33953388
3396- elif eval_type == PythonEvalType .SQL_WINDOW_AGG_ARROW_UDF :
3397- import pyarrow as pa
3398-
3399- # For SQL_WINDOW_AGG_ARROW_UDF,
3400- # convert iterator of batch columns to a concatenated RecordBatch
3401- def mapper (a ):
3402- # a is Iterator[Tuple[pa.Array, ...]] - convert to RecordBatch
3403- batches = []
3404- for batch_columns in a :
3405- # batch_columns is Tuple[pa.Array, ...] - convert to RecordBatch
3406- batch = pa .RecordBatch .from_arrays (
3407- batch_columns , names = ["_%d" % i for i in range (len (batch_columns ))]
3408- )
3409- batches .append (batch )
3410-
3411- # Concatenate all batches into one
3412- if hasattr (pa , "concat_batches" ):
3413- concatenated_batch = pa .concat_batches (batches )
3414- else :
3415- # pyarrow.concat_batches not supported before 19.0.0
3416- # remove this once we drop support for old versions
3417- concatenated_batch = pa .RecordBatch .from_struct_array (
3418- pa .concat_arrays ([b .to_struct_array () for b in batches ])
3419- )
3420-
3421- # Extract series using offsets (concatenated_batch.columns[o] gives pa.Array)
3422- result = tuple (
3423- f (* [concatenated_batch .columns [o ] for o in arg_offsets ]) for arg_offsets , f in udfs
3424- )
3425- # In the special case of a single UDF this will return a single result rather
3426- # than a tuple of results; this is the format that the JVM side expects.
3427- if len (result ) == 1 :
3428- return result [0 ]
3429- else :
3430- return result
3431-
34323389 elif eval_type in (
34333390 PythonEvalType .SQL_GROUPED_AGG_PANDAS_UDF ,
34343391 PythonEvalType .SQL_WINDOW_AGG_PANDAS_UDF ,
0 commit comments