blob: 78d94208d86c75d6325aa01c524fd4016d32b0c5 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use arrow::array::{
as_dictionary_array, make_array, Array, ArrayData, ArrayRef, DictionaryArray,
GenericStringArray, Int32Array, OffsetSizeTrait,
};
use arrow::buffer::MutableBuffer;
use arrow::datatypes::{DataType, Int32Type};
use datafusion::common::{exec_err, internal_datafusion_err, DataFusionError, Result, ScalarValue};
use datafusion::logical_expr::{
ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, Volatility,
};
use std::{any::Any, sync::Arc};
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct SparkStringSpace {
signature: Signature,
aliases: Vec<String>,
}
impl Default for SparkStringSpace {
fn default() -> Self {
Self::new()
}
}
impl SparkStringSpace {
pub fn new() -> Self {
Self {
signature: Signature::user_defined(Volatility::Immutable),
aliases: vec![],
}
}
}
impl ScalarUDFImpl for SparkStringSpace {
fn as_any(&self) -> &dyn Any {
self
}
fn name(&self) -> &str {
"string_space"
}
fn signature(&self) -> &Signature {
&self.signature
}
fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
Ok(match &arg_types[0] {
DataType::Dictionary(key_type, _) => {
DataType::Dictionary(key_type.clone(), Box::new(DataType::Utf8))
}
_ => DataType::Utf8,
})
}
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
let args: [ColumnarValue; 1] = args
.args
.try_into()
.map_err(|_| internal_datafusion_err!("string_space expects exactly one argument"))?;
spark_string_space(&args)
}
fn aliases(&self) -> &[String] {
&self.aliases
}
}
pub fn spark_string_space(args: &[ColumnarValue; 1]) -> Result<ColumnarValue> {
match args {
[ColumnarValue::Array(array)] => {
let result = string_space_array(&array)?;
Ok(ColumnarValue::Array(result))
}
[ColumnarValue::Scalar(scalar)] => {
let result = string_space_scalar(scalar)?;
Ok(ColumnarValue::Scalar(result))
}
}
}
fn string_space_array(length: &dyn Array) -> std::result::Result<ArrayRef, DataFusionError> {
match length.data_type() {
DataType::Int32 => {
let array = length.as_any().downcast_ref::<Int32Array>().unwrap();
Ok(generic_string_space::<i32>(array))
}
DataType::Dictionary(_, _) => {
let dict = as_dictionary_array::<Int32Type>(length);
let values = string_space_array(dict.values())?;
let result = DictionaryArray::try_new(dict.keys().clone(), values)?;
Ok(Arc::new(result))
}
other => exec_err!("Unsupported input type for function 'string_space': {other:?}"),
}
}
fn string_space_scalar(scalar: &ScalarValue) -> Result<ScalarValue> {
match scalar {
ScalarValue::Int32(value) => {
let result = value.map(|v| {
if v <= 0 {
String::new()
} else {
" ".repeat(v as usize)
}
});
Ok(ScalarValue::Utf8(result))
}
other => {
exec_err!("Unsupported data type {other:?} for function `space`")
}
}
}
fn generic_string_space<OffsetSize: OffsetSizeTrait>(length: &Int32Array) -> ArrayRef {
let array_len = length.len();
let mut offsets = MutableBuffer::new((array_len + 1) * std::mem::size_of::<OffsetSize>());
let mut length_so_far = OffsetSize::zero();
// compute null bitmap (copy)
let null_bit_buffer = length.to_data().nulls().map(|b| b.buffer().clone());
// Gets slice of length array to access it directly for performance.
// Negative length values are set to zero to match Spark behavior
let length_data = length.to_data();
let lengths: Vec<_> = length_data.buffers()[0]
.typed_data::<i32>()
.iter()
.map(|l| (*l).max(0) as usize)
.collect();
let total = lengths.iter().sum::<usize>();
let mut values = MutableBuffer::new(total);
offsets.push(length_so_far);
let blank = " ".as_bytes()[0];
values.resize(total, blank);
(0..array_len).for_each(|i| {
let current_len = lengths[i];
length_so_far += OffsetSize::from_usize(current_len).unwrap();
offsets.push(length_so_far);
});
let data = unsafe {
ArrayData::new_unchecked(
GenericStringArray::<OffsetSize>::DATA_TYPE,
array_len,
None,
null_bit_buffer,
0,
vec![offsets.into(), values.into()],
vec![],
)
};
make_array(data)
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::StringArray;
use datafusion::common::cast::as_string_array;
#[test]
fn test_negative_length() {
let input = Int32Array::from(vec![Some(-1), Some(-2), None]);
let args = ColumnarValue::Array(Arc::new(input));
match spark_string_space(&[args]) {
Ok(ColumnarValue::Array(result)) => {
let actual = as_string_array(&result).unwrap();
let expected = StringArray::from(vec![Some(""), Some(""), None]);
assert_eq!(actual, &expected)
}
_ => unreachable!(),
}
}
}