use crate::binaries::{
ConcatBinaryBuilder, ConcatBinaryViewBuilder, ConcatLargeBinaryBuilder,
};
use crate::string::concat;
use crate::strings::{
ColumnarValueRef, ConcatBuilder, ConcatLargeStringBuilder, ConcatStringBuilder,
ConcatStringViewBuilder, widest_binary_type, widest_string_type,
};
use arrow::array::Array;
use arrow::datatypes::DataType;
use datafusion_common::{
Result, ScalarValue, exec_datafusion_err, internal_err, plan_err,
};
use datafusion_expr::expr::ScalarFunction;
use datafusion_expr::simplify::{ExprSimplifyResult, SimplifyContext};
use datafusion_expr::{ColumnarValue, Documentation, Expr, Volatility, lit};
use datafusion_expr::{ScalarFunctionArgs, ScalarUDFImpl, Signature};
use datafusion_macros::user_doc;
#[user_doc(
doc_section(label = "String Functions"),
description = "Concatenates multiple strings together.",
syntax_example = "concat(str[, ..., str_n])",
sql_example = r#"```sql
> select concat('data', 'f', 'us', 'ion');
+-------------------------------------------------------+
| concat(Utf8("data"),Utf8("f"),Utf8("us"),Utf8("ion")) |
+-------------------------------------------------------+
| datafusion |
+-------------------------------------------------------+
```"#,
standard_argument(name = "str", prefix = "String"),
argument(
name = "str_n",
description = "Subsequent string expressions to concatenate."
),
related_udf(name = "concat_ws")
)]
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ConcatFunc {
signature: Signature,
}
impl Default for ConcatFunc {
fn default() -> Self {
ConcatFunc::new()
}
}
impl ConcatFunc {
pub fn new() -> Self {
Self {
signature: Signature::user_defined(Volatility::Immutable),
}
}
}
impl ScalarUDFImpl for ConcatFunc {
fn name(&self) -> &str {
"concat"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn coerce_types(&self, arg_types: &[DataType]) -> Result<Vec<DataType>> {
if arg_types.is_empty() {
plan_err!("concat does not support zero arguments")
} else {
coerce_arg_types(arg_types)
}
}
fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
Ok(deduce_return_type(arg_types))
}
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
let return_datatype = args.return_type().clone();
let ScalarFunctionArgs { args, .. } = args;
let array_len = args.iter().find_map(|x| match x {
ColumnarValue::Array(array) => Some(array.len()),
_ => None,
});
if array_len.is_none() {
let mut values: Vec<&[u8]> = Vec::with_capacity(args.len());
for arg in &args {
let ColumnarValue::Scalar(scalar) = arg else {
return internal_err!("concat expected scalar value, got {arg:?}");
};
if let ScalarValue::Binary(Some(value)) = scalar {
values.push(value);
} else if let ScalarValue::LargeBinary(Some(value)) = scalar {
values.push(value);
} else if let ScalarValue::BinaryView(Some(value)) = scalar {
values.push(value);
} else if scalar.is_null() {
} else {
match scalar.try_as_str() {
Some(Some(v)) => values.push(v.as_bytes()),
Some(None) => {} None => plan_err!(
"Concat function does not support scalar type {}",
scalar
)?,
}
}
}
let concat_bytes = values.concat();
return match return_datatype {
DataType::Utf8View => {
let result = std::str::from_utf8(&concat_bytes)
.map_err(|_| {
exec_datafusion_err!("invalid UTF-8 in binary literal")
})?
.to_string();
Ok(ColumnarValue::Scalar(ScalarValue::Utf8View(Some(result))))
}
DataType::Utf8 => {
let result = std::str::from_utf8(&concat_bytes)
.map_err(|_| {
exec_datafusion_err!("invalid UTF-8 in binary literal")
})?
.to_string();
Ok(ColumnarValue::Scalar(ScalarValue::Utf8(Some(result))))
}
DataType::LargeUtf8 => {
let result = std::str::from_utf8(&concat_bytes)
.map_err(|_| {
exec_datafusion_err!("invalid UTF-8 in binary literal")
})?
.to_string();
Ok(ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(result))))
}
DataType::Binary => Ok(ColumnarValue::Scalar(ScalarValue::Binary(Some(
concat_bytes,
)))),
DataType::LargeBinary => Ok(ColumnarValue::Scalar(
ScalarValue::LargeBinary(Some(concat_bytes)),
)),
DataType::BinaryView => Ok(ColumnarValue::Scalar(
ScalarValue::BinaryView(Some(concat_bytes)),
)),
other => {
plan_err!("Concat function does not support datatype of {other}")
}
};
}
let len = array_len.unwrap();
let mut data_size = 0;
let mut columns = Vec::with_capacity(args.len());
for arg in &args {
if let Some(column) =
ColumnarValueRef::from_columnar_value(arg, &mut data_size, len, 1, false)?
{
columns.push(column);
}
}
match return_datatype {
DataType::Utf8 => build_concat(
ConcatStringBuilder::with_capacity(len, data_size),
&columns,
len,
),
DataType::Utf8View => build_concat(
ConcatStringViewBuilder::with_capacity(len, data_size),
&columns,
len,
),
DataType::LargeUtf8 => build_concat(
ConcatLargeStringBuilder::with_capacity(len, data_size),
&columns,
len,
),
DataType::Binary => build_concat(
ConcatBinaryBuilder::with_capacity(len, data_size),
&columns,
len,
),
DataType::LargeBinary => build_concat(
ConcatLargeBinaryBuilder::with_capacity(len, data_size),
&columns,
len,
),
DataType::BinaryView => build_concat(
ConcatBinaryViewBuilder::with_capacity(len, data_size),
&columns,
len,
),
_ => unreachable!("concat"),
}
}
fn simplify(
&self,
args: Vec<Expr>,
_info: &SimplifyContext,
) -> Result<ExprSimplifyResult> {
simplify_concat(args)
}
fn documentation(&self) -> Option<&Documentation> {
self.doc()
}
}
pub(crate) fn deduce_return_type(arg_types: &[DataType]) -> DataType {
use DataType::*;
if arg_types.contains(&BinaryView) {
BinaryView
} else if arg_types.contains(&LargeBinary) {
LargeBinary
} else if arg_types.contains(&Binary) {
Binary
} else if arg_types.contains(&Utf8View) {
Utf8View
} else if arg_types.contains(&LargeUtf8) {
LargeUtf8
} else {
Utf8
}
}
pub(crate) fn coerce_arg_types(arg_types: &[DataType]) -> Result<Vec<DataType>> {
let has_binary = arg_types.iter().any(|dt| dt.is_binary());
let has_string = arg_types.iter().any(|dt| dt.is_string());
if has_binary && has_string {
Ok(vec![widest_string_type(arg_types); arg_types.len()])
} else if has_binary {
Ok(vec![widest_binary_type(arg_types); arg_types.len()])
} else {
Ok(vec![widest_string_type(arg_types); arg_types.len()])
}
}
fn build_concat<B: ConcatBuilder>(
mut builder: B,
columns: &[ColumnarValueRef],
len: usize,
) -> Result<ColumnarValue> {
for i in 0..len {
for column in columns {
builder.write::<true>(column, i)?;
}
builder.append_offset()?;
}
let array = builder.finish(None)?;
Ok(ColumnarValue::Array(array))
}
pub(crate) fn simplify_concat(args: Vec<Expr>) -> Result<ExprSimplifyResult> {
for arg in &args {
match arg {
Expr::Literal(dt, _) if dt.data_type().is_binary() => {
return Ok(ExprSimplifyResult::Original(args));
}
_ => {}
}
}
let mut new_args = Vec::with_capacity(args.len());
let mut contiguous_scalar = "".to_string();
let return_type = {
let data_types: Vec<_> = args
.iter()
.filter_map(|expr| match expr {
Expr::Literal(l, _) => Some(l.data_type()),
_ => None,
})
.collect();
ConcatFunc::new().return_type(&data_types)
}?;
for arg in args.clone() {
match arg {
Expr::Literal(ScalarValue::Utf8(None), _) => {}
Expr::Literal(ScalarValue::LargeUtf8(None), _) => {}
Expr::Literal(ScalarValue::Utf8View(None), _) => {}
Expr::Literal(ScalarValue::Utf8(Some(v)), _) => {
contiguous_scalar += &v;
}
Expr::Literal(ScalarValue::LargeUtf8(Some(v)), _) => {
contiguous_scalar += &v;
}
Expr::Literal(ScalarValue::Utf8View(Some(v)), _) => {
contiguous_scalar += &v;
}
Expr::Literal(x, _) => {
return internal_err!(
"The scalar {x} should be casted to string type during the type coercion."
);
}
arg => {
if !contiguous_scalar.is_empty() {
match return_type {
DataType::Utf8 => new_args.push(lit(contiguous_scalar)),
DataType::LargeUtf8 => new_args
.push(lit(ScalarValue::LargeUtf8(Some(contiguous_scalar)))),
DataType::Utf8View => new_args
.push(lit(ScalarValue::Utf8View(Some(contiguous_scalar)))),
_ => unreachable!(),
}
contiguous_scalar = "".to_string();
}
new_args.push(arg);
}
}
}
if !contiguous_scalar.is_empty() {
match return_type {
DataType::Utf8 => new_args.push(lit(contiguous_scalar)),
DataType::LargeUtf8 => {
new_args.push(lit(ScalarValue::LargeUtf8(Some(contiguous_scalar))))
}
DataType::Utf8View => {
new_args.push(lit(ScalarValue::Utf8View(Some(contiguous_scalar))))
}
_ => unreachable!(),
}
}
if !args.eq(&new_args) {
Ok(ExprSimplifyResult::Simplified(Expr::ScalarFunction(
ScalarFunction {
func: concat(),
args: new_args,
},
)))
} else {
Ok(ExprSimplifyResult::Original(args))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::test::test_function;
use DataType::*;
use arrow::array::{
ArrayRef, BinaryArray, BinaryViewArray, LargeBinaryArray, StringArray,
};
use arrow::array::{LargeStringArray, StringViewArray};
use arrow::datatypes::Field;
use datafusion_common::config::ConfigOptions;
use std::sync::Arc;
#[test]
fn test_functions() -> Result<()> {
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::from("aa")),
ColumnarValue::Scalar(ScalarValue::from("bb")),
ColumnarValue::Scalar(ScalarValue::from("cc")),
],
Ok(Some("aabbcc")),
&str,
Utf8,
StringArray
);
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::from("aa")),
ColumnarValue::Scalar(ScalarValue::Utf8(None)),
ColumnarValue::Scalar(ScalarValue::from("cc")),
],
Ok(Some("aacc")),
&str,
Utf8,
StringArray
);
test_function!(
ConcatFunc::new(),
vec![ColumnarValue::Scalar(ScalarValue::Utf8(None))],
Ok(Some("")),
&str,
Utf8,
StringArray
);
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::from("aa")),
ColumnarValue::Scalar(ScalarValue::Utf8View(None)),
ColumnarValue::Scalar(ScalarValue::LargeUtf8(None)),
ColumnarValue::Scalar(ScalarValue::from("cc")),
],
Ok(Some("aacc")),
&str,
Utf8View,
StringViewArray
);
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::from("aa")),
ColumnarValue::Scalar(ScalarValue::LargeUtf8(None)),
ColumnarValue::Scalar(ScalarValue::from("cc")),
],
Ok(Some("aacc")),
&str,
LargeUtf8,
LargeStringArray
);
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::Utf8View(Some("aa".to_string()))),
ColumnarValue::Scalar(ScalarValue::Utf8(Some("cc".to_string()))),
],
Ok(Some("aacc")),
&str,
Utf8View,
StringViewArray
);
Ok(())
}
#[test]
fn test_scalar_binary() -> Result<()> {
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::Binary(Some(
"Café".as_bytes().into()
))),
ColumnarValue::Scalar(ScalarValue::Binary(Some("cc".as_bytes().into()))),
],
Ok(Some("Cafécc".as_bytes())),
&[u8],
Binary,
BinaryArray
);
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::Binary(Some(
"Café".as_bytes().into()
))),
ColumnarValue::Scalar(ScalarValue::LargeBinary(Some(
"cc".as_bytes().into()
))),
],
Ok(Some("Cafécc".as_bytes())),
&[u8],
LargeBinary,
LargeBinaryArray
);
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::Binary(Some(
"Café".as_bytes().into()
))),
ColumnarValue::Scalar(ScalarValue::BinaryView(Some(
"cc".as_bytes().into()
))),
],
Ok(Some("Cafécc".as_bytes())),
&[u8],
BinaryView,
BinaryViewArray
);
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::BinaryView(Some(
"Café".as_bytes().into()
))),
ColumnarValue::Scalar(ScalarValue::BinaryView(Some(
"cc".as_bytes().into()
))),
],
Ok(Some("Cafécc".as_bytes())),
&[u8],
BinaryView,
BinaryViewArray
);
test_function!(
ConcatFunc::new(),
vec![
ColumnarValue::Scalar(ScalarValue::Binary(None)),
ColumnarValue::Scalar(ScalarValue::Binary(Some(b"hello".to_vec()))),
],
Ok(Some(b"hello".as_ref())),
&[u8],
Binary,
BinaryArray
);
test_function!(
ConcatFunc::new(),
vec![ColumnarValue::Scalar(ScalarValue::Binary(None))],
Ok(Some(b"".as_ref())),
&[u8],
Binary,
BinaryArray
);
Ok(())
}
#[test]
fn test_array_string() -> Result<()> {
let c0 =
ColumnarValue::Array(Arc::new(StringArray::from(vec!["foo", "bar", "baz"])));
let c1 = ColumnarValue::Scalar(ScalarValue::Utf8(Some(",".to_string())));
let c2 = ColumnarValue::Array(Arc::new(StringArray::from(vec![
Some("x"),
None,
Some("z"),
])));
let c3 = ColumnarValue::Scalar(ScalarValue::Utf8View(Some(",".to_string())));
let c4 = ColumnarValue::Array(Arc::new(StringViewArray::from(vec![
Some("a"),
None,
Some("b"),
])));
let arg_fields = vec![
Field::new("a", Utf8, true),
Field::new("a", Utf8, true),
Field::new("a", Utf8, true),
Field::new("a", Utf8View, true),
Field::new("a", Utf8View, true),
]
.into_iter()
.map(Arc::new)
.collect::<Vec<_>>();
let args = ScalarFunctionArgs {
args: vec![c0, c1, c2, c3, c4],
arg_fields,
number_rows: 3,
return_field: Field::new("f", Utf8View, true).into(),
config_options: Arc::new(ConfigOptions::default()),
};
let result = ConcatFunc::new().invoke_with_args(args)?;
let expected =
Arc::new(StringViewArray::from(vec!["foo,x,a", "bar,,", "baz,z,b"]))
as ArrayRef;
match &result {
ColumnarValue::Array(array) => {
assert_eq!(&expected, array);
}
_ => panic!(),
}
Ok(())
}
#[test]
fn test_array_binary() -> Result<()> {
let c0 = ColumnarValue::Array(Arc::new(BinaryArray::from_vec(vec![
b"foo", b"bar", b"baz",
])));
let c1 = ColumnarValue::Scalar(ScalarValue::LargeBinary(Some(b",".to_vec())));
let c2 = ColumnarValue::Array(Arc::new(BinaryArray::from_opt_vec(vec![
Some(b"x"),
None,
Some(b"z"),
])));
let c3 = ColumnarValue::Scalar(ScalarValue::BinaryView(Some(b",".to_vec())));
let c4 = ColumnarValue::Array(Arc::new(BinaryViewArray::from_iter(vec![
Some(b"a"),
None,
Some(b"b"),
])));
let arg_fields = vec![
Field::new("a", Binary, true),
Field::new("a", LargeBinary, true),
Field::new("a", Binary, true),
Field::new("a", BinaryView, true),
Field::new("a", BinaryView, true),
]
.into_iter()
.map(Arc::new)
.collect::<Vec<_>>();
let args = ScalarFunctionArgs {
args: vec![c0, c1, c2, c3, c4],
arg_fields,
number_rows: 3,
return_field: Field::new("f", BinaryView, true).into(),
config_options: Arc::new(ConfigOptions::default()),
};
let result = ConcatFunc::new().invoke_with_args(args)?;
let expected = Arc::new(BinaryViewArray::from_iter(vec![
Some(b"foo,x,a".to_vec()),
Some(b"bar,,".to_vec()),
Some(b"baz,z,b".to_vec()),
])) as ArrayRef;
match &result {
ColumnarValue::Array(array) => {
assert_eq!(&expected, array);
}
_ => panic!(),
}
Ok(())
}
}