2626//! - Always returns Utf8 (Spark's `STRING` type)
2727//! - Coerces non-string scalars (numbers, booleans, dates, ...) to Utf8
2828
29+ use std:: fmt:: Write as _;
2930use std:: sync:: Arc ;
3031
3132use arrow:: array:: {
3233 Array , ArrayRef , AsArray , GenericListArray , LargeStringArray , OffsetSizeTrait ,
3334 StringArray , StringBuilder , StringViewArray ,
3435} ;
35- use arrow:: datatypes:: DataType ;
36+ use arrow:: datatypes:: { DataType , Field } ;
3637use datafusion_common:: { Result , ScalarValue } ;
3738use datafusion_expr:: {
3839 ColumnarValue , ScalarFunctionArgs , ScalarUDFImpl , Signature , Volatility ,
@@ -83,17 +84,28 @@ impl ScalarUDFImpl for SparkConcatWs {
8384 . enumerate ( )
8485 . map ( |( i, dt) | match dt {
8586 DataType :: Utf8 | DataType :: LargeUtf8 | DataType :: Utf8View => dt. clone ( ) ,
86- // Non-separator list args expand their elements at runtime;
87- // normalize list variants so the kernel only sees List/LargeList.
87+ // Non-separator list args expand their elements at runtime.
88+ // Normalize the list variant so the kernel only sees
89+ // List/LargeList, AND force the element type to Utf8 so the
90+ // planner inserts a cast for non-string children (Spark
91+ // coerces them to STRING the same way it does for scalars).
8892 DataType :: List ( f)
8993 | DataType :: ListView ( f)
9094 | DataType :: FixedSizeList ( f, _)
9195 if i > 0 =>
9296 {
93- DataType :: List ( Arc :: clone ( f) )
97+ DataType :: List ( Arc :: new ( Field :: new (
98+ f. name ( ) ,
99+ DataType :: Utf8 ,
100+ f. is_nullable ( ) ,
101+ ) ) )
94102 }
95103 DataType :: LargeList ( f) | DataType :: LargeListView ( f) if i > 0 => {
96- DataType :: LargeList ( Arc :: clone ( f) )
104+ DataType :: LargeList ( Arc :: new ( Field :: new (
105+ f. name ( ) ,
106+ DataType :: Utf8 ,
107+ f. is_nullable ( ) ,
108+ ) ) )
97109 }
98110 // Spark casts everything else (numbers, booleans, dates,
99111 // binary, null...) to STRING.
@@ -137,37 +149,29 @@ fn only_separator(sep: &ColumnarValue) -> Result<ColumnarValue> {
137149
138150fn spark_concat_ws ( args : & [ ColumnarValue ] , num_rows : usize ) -> Result < ColumnarValue > {
139151 let arrays = ColumnarValue :: values_to_arrays ( args) ?;
140-
141- // Untyped-NULL separator → every row is NULL. Returning a scalar is enough;
142- // the framework broadcasts it to `num_rows` nulls when needed.
143- if * arrays[ 0 ] . data_type ( ) == DataType :: Null {
144- return Ok ( ColumnarValue :: Scalar ( ScalarValue :: Utf8 ( None ) ) ) ;
145- }
146-
147152 let sep_view = StringView :: try_new ( & arrays[ 0 ] ) ?;
148153 let arg_views: Vec < ArgView > = arrays[ 1 ..]
149154 . iter ( )
150155 . map ( ArgView :: try_new)
151156 . collect :: < Result < _ > > ( ) ?;
152157
153158 let mut builder = StringBuilder :: with_capacity ( num_rows, num_rows * 16 ) ;
154- let mut buf = String :: new ( ) ;
155159
156160 for row_idx in 0 ..num_rows {
157161 if sep_view. is_null ( row_idx) {
158162 builder. append_null ( ) ;
159163 continue ;
160164 }
161165
166+ // Write parts directly into the builder via its `fmt::Write` impl;
167+ // `append_value("")` then finalises the row (offset + validity) with
168+ // no extra copy from an intermediate `String`.
162169 let separator = sep_view. value ( row_idx) ;
163- buf. clear ( ) ;
164170 let mut first = true ;
165-
166171 for view in & arg_views {
167- view. write_row ( row_idx, separator, & mut buf , & mut first) ?;
172+ view. write_row ( row_idx, separator, & mut builder , & mut first) ?;
168173 }
169-
170- builder. append_value ( & buf) ;
174+ builder. append_value ( "" ) ;
171175 }
172176
173177 Ok ( ColumnarValue :: Array ( Arc :: new ( builder. finish ( ) ) as ArrayRef ) )
@@ -208,10 +212,10 @@ impl<'a> StringView<'a> {
208212 }
209213}
210214
211- /// Per-argument view: a string array, a list of strings, or an all-null
212- /// argument. The downcast happens once at construction time.
215+ /// Per-argument view: a string array or a list of strings. The downcast
216+ /// happens once at construction time. `DataType::Null` cannot appear here —
217+ /// `coerce_types` rewrites it to `Utf8` before invocation.
213218enum ArgView < ' a > {
214- Null ,
215219 Str ( StringView < ' a > ) ,
216220 List ( & ' a GenericListArray < i32 > ) ,
217221 LargeList ( & ' a GenericListArray < i64 > ) ,
@@ -220,7 +224,6 @@ enum ArgView<'a> {
220224impl < ' a > ArgView < ' a > {
221225 fn try_new ( arr : & ' a ArrayRef ) -> Result < Self > {
222226 match arr. data_type ( ) {
223- DataType :: Null => Ok ( Self :: Null ) ,
224227 DataType :: Utf8 | DataType :: LargeUtf8 | DataType :: Utf8View => {
225228 Ok ( Self :: Str ( StringView :: try_new ( arr) ?) )
226229 }
@@ -238,18 +241,17 @@ impl<'a> ArgView<'a> {
238241 & self ,
239242 row_idx : usize ,
240243 sep : & str ,
241- buf : & mut String ,
244+ builder : & mut StringBuilder ,
242245 first : & mut bool ,
243246 ) -> Result < ( ) > {
244247 match self {
245- Self :: Null => { }
246248 Self :: Str ( view) => {
247249 if !view. is_null ( row_idx) {
248- push_part ( buf , view. value ( row_idx) , sep, first) ;
250+ push_part ( builder , view. value ( row_idx) , sep, first) ;
249251 }
250252 }
251- Self :: List ( list) => write_list_row ( * list, row_idx, sep, buf , first) ?,
252- Self :: LargeList ( list) => write_list_row ( * list, row_idx, sep, buf , first) ?,
253+ Self :: List ( list) => write_list_row ( * list, row_idx, sep, builder , first) ?,
254+ Self :: LargeList ( list) => write_list_row ( * list, row_idx, sep, builder , first) ?,
253255 }
254256 Ok ( ( ) )
255257 }
@@ -259,31 +261,37 @@ fn write_list_row<O: OffsetSizeTrait>(
259261 list : & GenericListArray < O > ,
260262 row_idx : usize ,
261263 sep : & str ,
262- buf : & mut String ,
264+ builder : & mut StringBuilder ,
263265 first : & mut bool ,
264266) -> Result < ( ) > {
265267 if list. is_null ( row_idx) {
266268 return Ok ( ( ) ) ;
267269 }
268270 let values = list. value ( row_idx) ;
269- // An empty array (e.g. `array()`) or an all-null-typed array contributes
270- // nothing — Spark renders it as the empty string, not an error.
271- if values. is_empty ( ) || * values . data_type ( ) == DataType :: Null {
271+ // An empty array (e.g. `array()`) contributes nothing — Spark renders it
272+ // as the empty string, not an error.
273+ if values. is_empty ( ) {
272274 return Ok ( ( ) ) ;
273275 }
274276 let view = StringView :: try_new ( & values) ?;
275277 for i in 0 ..values. len ( ) {
276278 if !view. is_null ( i) {
277- push_part ( buf , view. value ( i) , sep, first) ;
279+ push_part ( builder , view. value ( i) , sep, first) ;
278280 }
279281 }
280282 Ok ( ( ) )
281283}
282284
283- fn push_part ( buf : & mut String , part : & str , sep : & str , first : & mut bool ) {
285+ // `StringBuilder::write_str` only does `extend_from_slice` and never errors;
286+ // the `.expect(..)` is a documentation hint, not a real failure path.
287+ fn push_part ( builder : & mut StringBuilder , part : & str , sep : & str , first : & mut bool ) {
284288 if !* first {
285- buf. push_str ( sep) ;
289+ builder
290+ . write_str ( sep)
291+ . expect ( "StringBuilder::write_str is infallible" ) ;
286292 }
287293 * first = false ;
288- buf. push_str ( part) ;
294+ builder
295+ . write_str ( part)
296+ . expect ( "StringBuilder::write_str is infallible" ) ;
289297}
0 commit comments