use std::fmt::Write as _;
use std::sync::Arc;
use arrow::array::{
Array, ArrayRef, AsArray, GenericListArray, LargeStringArray, OffsetSizeTrait,
StringArray, StringBuilder, StringViewArray,
};
use arrow::datatypes::{DataType, Field};
use datafusion_common::{Result, ScalarValue};
use datafusion_expr::{
ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
};
use crate::function::error_utils::{
invalid_arg_count_exec_err, unsupported_data_type_exec_err,
};
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct SparkConcatWs {
signature: Signature,
}
impl Default for SparkConcatWs {
fn default() -> Self {
Self::new()
}
}
impl SparkConcatWs {
pub fn new() -> Self {
Self {
signature: Signature::user_defined(Volatility::Immutable),
}
}
}
impl ScalarUDFImpl for SparkConcatWs {
fn name(&self) -> &str {
"concat_ws"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
Ok(DataType::Utf8)
}
fn coerce_types(&self, arg_types: &[DataType]) -> Result<Vec<DataType>> {
if arg_types.is_empty() {
return Err(invalid_arg_count_exec_err("concat_ws", (1, i32::MAX), 0));
}
Ok(arg_types
.iter()
.enumerate()
.map(|(i, dt)| match dt {
DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => dt.clone(),
DataType::List(f)
| DataType::ListView(f)
| DataType::FixedSizeList(f, _)
if i > 0 =>
{
DataType::List(Arc::new(Field::new(
f.name(),
DataType::Utf8,
f.is_nullable(),
)))
}
DataType::LargeList(f) | DataType::LargeListView(f) if i > 0 => {
DataType::LargeList(Arc::new(Field::new(
f.name(),
DataType::Utf8,
f.is_nullable(),
)))
}
_ => DataType::Utf8,
})
.collect())
}
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
if args.args.len() == 1 {
return only_separator(&args.args[0]);
}
spark_concat_ws(&args.args, args.number_rows)
}
}
fn only_separator(sep: &ColumnarValue) -> Result<ColumnarValue> {
match sep {
ColumnarValue::Scalar(s) if s.is_null() => {
Ok(ColumnarValue::Scalar(ScalarValue::Utf8(None)))
}
ColumnarValue::Scalar(_) => Ok(ColumnarValue::Scalar(ScalarValue::Utf8(Some(
String::new(),
)))),
ColumnarValue::Array(arr) => {
let mut builder = StringBuilder::with_capacity(arr.len(), 0);
for row_idx in 0..arr.len() {
if arr.is_null(row_idx) {
builder.append_null();
} else {
builder.append_value("");
}
}
Ok(ColumnarValue::Array(Arc::new(builder.finish()) as ArrayRef))
}
}
}
fn spark_concat_ws(args: &[ColumnarValue], num_rows: usize) -> Result<ColumnarValue> {
let arrays = ColumnarValue::values_to_arrays(args)?;
let sep_view = StringView::try_new(&arrays[0])?;
let arg_views: Vec<ArgView> = arrays[1..]
.iter()
.map(ArgView::try_new)
.collect::<Result<_>>()?;
let mut builder = StringBuilder::with_capacity(num_rows, num_rows * 16);
for row_idx in 0..num_rows {
if sep_view.is_null(row_idx) {
builder.append_null();
continue;
}
let separator = sep_view.value(row_idx);
let mut first = true;
for view in &arg_views {
view.write_row(row_idx, separator, &mut builder, &mut first)?;
}
builder.append_value("");
}
Ok(ColumnarValue::Array(Arc::new(builder.finish()) as ArrayRef))
}
enum StringView<'a> {
Utf8(&'a StringArray),
LargeUtf8(&'a LargeStringArray),
Utf8View(&'a StringViewArray),
}
impl<'a> StringView<'a> {
fn try_new(arr: &'a ArrayRef) -> Result<Self> {
match arr.data_type() {
DataType::Utf8 => Ok(Self::Utf8(arr.as_string::<i32>())),
DataType::LargeUtf8 => Ok(Self::LargeUtf8(arr.as_string::<i64>())),
DataType::Utf8View => Ok(Self::Utf8View(arr.as_string_view())),
other => Err(unsupported_data_type_exec_err("concat_ws", "STRING", other)),
}
}
fn value(&self, idx: usize) -> &str {
match self {
Self::Utf8(a) => a.value(idx),
Self::LargeUtf8(a) => a.value(idx),
Self::Utf8View(a) => a.value(idx),
}
}
fn is_null(&self, idx: usize) -> bool {
match self {
Self::Utf8(a) => a.is_null(idx),
Self::LargeUtf8(a) => a.is_null(idx),
Self::Utf8View(a) => a.is_null(idx),
}
}
}
enum ArgView<'a> {
Str(StringView<'a>),
List(&'a GenericListArray<i32>),
LargeList(&'a GenericListArray<i64>),
}
impl<'a> ArgView<'a> {
fn try_new(arr: &'a ArrayRef) -> Result<Self> {
match arr.data_type() {
DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => {
Ok(Self::Str(StringView::try_new(arr)?))
}
DataType::List(_) => Ok(Self::List(arr.as_list::<i32>())),
DataType::LargeList(_) => Ok(Self::LargeList(arr.as_list::<i64>())),
other => Err(unsupported_data_type_exec_err(
"concat_ws",
"STRING or ARRAY<STRING>",
other,
)),
}
}
fn write_row(
&self,
row_idx: usize,
sep: &str,
builder: &mut StringBuilder,
first: &mut bool,
) -> Result<()> {
match self {
Self::Str(view) => {
if !view.is_null(row_idx) {
push_part(builder, view.value(row_idx), sep, first);
}
}
Self::List(list) => write_list_row(*list, row_idx, sep, builder, first)?,
Self::LargeList(list) => write_list_row(*list, row_idx, sep, builder, first)?,
}
Ok(())
}
}
fn write_list_row<O: OffsetSizeTrait>(
list: &GenericListArray<O>,
row_idx: usize,
sep: &str,
builder: &mut StringBuilder,
first: &mut bool,
) -> Result<()> {
if list.is_null(row_idx) {
return Ok(());
}
let values = list.value(row_idx);
if values.is_empty() {
return Ok(());
}
let view = StringView::try_new(&values)?;
for i in 0..values.len() {
if !view.is_null(i) {
push_part(builder, view.value(i), sep, first);
}
}
Ok(())
}
fn push_part(builder: &mut StringBuilder, part: &str, sep: &str, first: &mut bool) {
if !*first {
builder
.write_str(sep)
.expect("StringBuilder::write_str is infallible");
}
*first = false;
builder
.write_str(part)
.expect("StringBuilder::write_str is infallible");
}