Skip to content

Commit a9538af

Browse files
committed
fix revision
1 parent c24665c commit a9538af

2 files changed

Lines changed: 69 additions & 35 deletions

File tree

datafusion/spark/src/function/string/concat_ws.rs

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@
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 _;
2930
use std::sync::Arc;
3031

3132
use 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};
3637
use datafusion_common::{Result, ScalarValue};
3738
use 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

138150
fn 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.
213218
enum 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> {
220224
impl<'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
}

datafusion/sqllogictest/test_files/spark/string/concat_ws.slt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,32 @@ NULL
354354
NULL
355355
NULL
356356

357+
## ── Non-string list elements (planner-inserted element cast) ─
358+
359+
## Array of integers — elements must be cast to STRING
360+
query T
361+
SELECT concat_ws(',', array(1, 2, 3));
362+
----
363+
1,2,3
364+
365+
## Array of doubles
366+
query T
367+
SELECT concat_ws('-', array(1.5, 2.5, 3.5));
368+
----
369+
1.5-2.5-3.5
370+
371+
## Array of booleans
372+
query T
373+
SELECT concat_ws(',', array(true, false, true));
374+
----
375+
true,false,true
376+
377+
## Mixed: string scalar + int array + string scalar
378+
query T
379+
SELECT concat_ws(',', 'x', array(1, 2), 'y');
380+
----
381+
x,1,2,y
382+
357383
## ── Struct rejection ────────────────────────────────────────
358384

359385
## Struct argument is rejected (not coerced to string)

0 commit comments

Comments
 (0)