Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion datafusion/functions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -407,4 +407,4 @@ required-features = ["math_expressions"]
[[bench]]
harness = false
name = "dictionary_encoding"
required-features = ["string_expressions"]
required-features = ["string_expressions", "unicode_expressions"]
8 changes: 7 additions & 1 deletion datafusion/functions/benches/dictionary_encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ fn create_string_dictionary(cardinality: usize) -> ArrayRef {
}

fn benchmark_dictionary_string_udfs(c: &mut Criterion) {
let udfs: [(&str, Arc<ScalarUDF>); 3] = [
let udfs: [(&str, Arc<ScalarUDF>); 6] = [
("ascii", datafusion_functions::string::ascii()),
("bit_length", datafusion_functions::string::bit_length()),
(
"character_length",
datafusion_functions::unicode::character_length(),
),
("initcap", datafusion_functions::unicode::initcap()),
("octet_length", datafusion_functions::string::octet_length()),
("reverse", datafusion_functions::unicode::reverse()),
];
let config_options = Arc::new(ConfigOptions::default());

Expand Down
31 changes: 23 additions & 8 deletions datafusion/functions/src/unicode/character_length.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@
// specific language governing permissions and limitations
// under the License.

use crate::utils::{make_scalar_function, utf8_to_int_type};
use crate::utils::{
make_scalar_function, transform_leaf_type_preserving_encoding, utf8_to_int_type,
};
use arrow::array::{
Array, ArrayRef, ArrowPrimitiveType, AsArray, OffsetSizeTrait, PrimitiveArray,
StringArrayType,
};
use arrow::datatypes::{ArrowNativeType, DataType, Int32Type, Int64Type};
use datafusion_common::Result;
use datafusion_common::types::{NativeType, logical_binary, logical_string};
use datafusion_expr::{
ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
Volatility,
Coercion, ColumnarValue, Documentation, EncodingPreservation, ScalarFunctionArgs,
ScalarUDFImpl, Signature, TypeSignatureClass, Volatility,
};
use datafusion_macros::user_doc;
use std::sync::Arc;
Expand Down Expand Up @@ -59,11 +62,16 @@ impl Default for CharacterLengthFunc {

impl CharacterLengthFunc {
pub fn new() -> Self {
use DataType::*;
Self {
signature: Signature::uniform(
1,
vec![Utf8, LargeUtf8, Utf8View],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this allowed any type;

> select character_length(10);
+-----------------------------+
| character_length(Int64(10)) |
+-----------------------------+
| 2                           |
+-----------------------------+
1 row(s) fetched.
Elapsed 0.004 seconds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this narrows the set of accepted inputs, but some of the implicit coercions allowed by the previous signatures don’t seem very intuitive to me. For example, reverse([1, 2, 3]) returns "]3 ,2 ,1[", and reverse(true) returns "eurt".

I checked postgresql and duckdb, and both require an explicit string cast here. This behavior seems more reasonable to me.

select reverse(true);
ERROR: function reverse(boolean) does not exist
HINT: You might need to add explicit type casts.

select reverse(ARRAY[1,2,3]);
ERROR: function reverse(integer[]) does not exist
HINT: You might need to add explicit type casts.

so I’m not sure whether we should keep the existing implicit conversions or require an explicit cast?

signature: Signature::coercible(
vec![
Coercion::new_implicit(
TypeSignatureClass::Native(logical_string()),
vec![TypeSignatureClass::Native(logical_binary())],
NativeType::String,
)
.with_encoding_preservation(EncodingPreservation::dictionary()),
],
Volatility::Immutable,
),
aliases: vec![String::from("length"), String::from("char_length")],
Expand All @@ -81,7 +89,9 @@ impl ScalarUDFImpl for CharacterLengthFunc {
}

fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
utf8_to_int_type(&arg_types[0], "character_length")
transform_leaf_type_preserving_encoding(&arg_types[0], &|data_type| {
utf8_to_int_type(data_type, "character_length")
})
}

fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
Expand Down Expand Up @@ -114,6 +124,11 @@ fn character_length(args: &[ArrayRef]) -> Result<ArrayRef> {
let string_array = args[0].as_string_view();
character_length_general::<Int32Type, _>(&string_array)
}
DataType::Dictionary(_, _) => {
let dictionary = args[0].as_any_dictionary();
let converted = character_length(&[Arc::clone(dictionary.values())])?;
Ok(dictionary.with_values(converted))
}
_ => unreachable!("CharacterLengthFunc"),
}
}
Expand Down
115 changes: 63 additions & 52 deletions datafusion/functions/src/unicode/initcap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,17 @@

use std::sync::Arc;

use arrow::array::{Array, ArrayRef, GenericStringArray, OffsetSizeTrait};
use arrow::array::{Array, ArrayRef, AsArray, GenericStringArray, OffsetSizeTrait};
use arrow::buffer::Buffer;
use arrow::datatypes::DataType;

use crate::strings::{GenericStringArrayBuilder, StringViewArrayBuilder};
use crate::utils::{make_scalar_function, utf8_to_str_type};
use datafusion_common::cast::{as_generic_string_array, as_string_view_array};
use datafusion_common::types::logical_string;
use datafusion_common::{Result, ScalarValue, exec_err};
use datafusion_expr::{
Coercion, ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
TypeSignatureClass, Volatility,
Coercion, ColumnarValue, Documentation, EncodingPreservation, ScalarFunctionArgs,
ScalarUDFImpl, Signature, TypeSignatureClass, Volatility,
};
use datafusion_macros::user_doc;

Expand Down Expand Up @@ -64,9 +63,10 @@ impl InitcapFunc {
pub fn new() -> Self {
Self {
signature: Signature::coercible(
vec![Coercion::new_exact(TypeSignatureClass::Native(
logical_string(),
))],
vec![
Coercion::new_exact(TypeSignatureClass::Native(logical_string()))
.with_encoding_preservation(EncodingPreservation::dictionary()),
],
Volatility::Immutable,
),
}
Expand All @@ -83,54 +83,16 @@ impl ScalarUDFImpl for InitcapFunc {
}

fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
if let DataType::Utf8View = arg_types[0] {
Ok(DataType::Utf8View)
} else {
utf8_to_str_type(&arg_types[0], "initcap")
}
Ok(arg_types[0].clone())
}

fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
let arg = &args.args[0];

// Scalar fast path - handle directly without array conversion
if let ColumnarValue::Scalar(scalar) = arg {
return match scalar {
ScalarValue::Utf8(None)
| ScalarValue::LargeUtf8(None)
| ScalarValue::Utf8View(None) => Ok(arg.clone()),
ScalarValue::Utf8(Some(s)) => {
let mut result = String::new();
initcap_string(s, &mut result);
Ok(ColumnarValue::Scalar(ScalarValue::Utf8(Some(result))))
}
ScalarValue::LargeUtf8(Some(s)) => {
let mut result = String::new();
initcap_string(s, &mut result);
Ok(ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(result))))
}
ScalarValue::Utf8View(Some(s)) => {
let mut result = String::new();
initcap_string(s, &mut result);
Ok(ColumnarValue::Scalar(ScalarValue::Utf8View(Some(result))))
}
other => {
exec_err!(
"Unsupported data type {:?} for function `initcap`",
other.data_type()
)
}
};
}

// Array path
let args = &args.args;
match args[0].data_type() {
DataType::Utf8 => make_scalar_function(initcap::<i32>, vec![])(args),
DataType::LargeUtf8 => make_scalar_function(initcap::<i64>, vec![])(args),
DataType::Utf8View => make_scalar_function(initcap_utf8view, vec![])(args),
other => {
exec_err!("Unsupported data type {other:?} for function `initcap`")
match &args.args[0] {
ColumnarValue::Scalar(scalar) => {
Ok(ColumnarValue::Scalar(initcap_scalar(scalar)?))
}
ColumnarValue::Array(array) => {
Ok(ColumnarValue::Array(initcap_array(array)?))
}
}
}
Expand All @@ -140,6 +102,55 @@ impl ScalarUDFImpl for InitcapFunc {
}
}

fn initcap_scalar(scalar: &ScalarValue) -> Result<ScalarValue> {
match scalar {
ScalarValue::Utf8(None)
| ScalarValue::LargeUtf8(None)
| ScalarValue::Utf8View(None) => Ok(scalar.clone()),
ScalarValue::Utf8(Some(s)) => {
let mut result = String::new();
initcap_string(s, &mut result);
Ok(ScalarValue::Utf8(Some(result)))
}
ScalarValue::LargeUtf8(Some(s)) => {
let mut result = String::new();
initcap_string(s, &mut result);
Ok(ScalarValue::LargeUtf8(Some(result)))
}
ScalarValue::Utf8View(Some(s)) => {
let mut result = String::new();
initcap_string(s, &mut result);
Ok(ScalarValue::Utf8View(Some(result)))
}
ScalarValue::Dictionary(key_type, value) => Ok(ScalarValue::Dictionary(
key_type.clone(),
Box::new(initcap_scalar(value)?),
)),
other => {
exec_err!(
"Unsupported data type {:?} for function `initcap`",
other.data_type()
)
}
}
}

fn initcap_array(array: &ArrayRef) -> Result<ArrayRef> {
match array.data_type() {
DataType::Utf8 => initcap::<i32>(&[Arc::clone(array)]),
DataType::LargeUtf8 => initcap::<i64>(&[Arc::clone(array)]),
DataType::Utf8View => initcap_utf8view(&[Arc::clone(array)]),
DataType::Dictionary(_, _) => {
let dictionary = array.as_any_dictionary();
let converted = initcap_array(dictionary.values())?;
Ok(dictionary.with_values(converted))
}
other => {
exec_err!("Unsupported data type {other:?} for function `initcap`")
}
}
}

/// Converts the first letter of each word to uppercase and the rest to
/// lowercase. Words are sequences of alphanumeric characters separated by
/// non-alphanumeric characters.
Expand Down
29 changes: 22 additions & 7 deletions datafusion/functions/src/unicode/reverse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@
// specific language governing permissions and limitations
// under the License.

use std::sync::Arc;

use crate::strings::{
BulkNullStringArrayBuilder, GenericStringArrayBuilder, StringViewArrayBuilder,
};
use crate::utils::make_scalar_function;
use DataType::{LargeUtf8, Utf8, Utf8View};
use arrow::array::{Array, ArrayRef, AsArray, StringArrayType};
use arrow::datatypes::DataType;
use datafusion_common::types::{NativeType, logical_binary, logical_string};
use datafusion_common::{Result, exec_err};
use datafusion_expr::{
ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
Volatility,
Coercion, ColumnarValue, Documentation, EncodingPreservation, ScalarFunctionArgs,
ScalarUDFImpl, Signature, TypeSignatureClass, Volatility,
};
use datafusion_macros::user_doc;

Expand Down Expand Up @@ -56,11 +59,16 @@ impl Default for ReverseFunc {

impl ReverseFunc {
pub fn new() -> Self {
use DataType::*;
Self {
signature: Signature::uniform(
1,
vec![Utf8View, Utf8, LargeUtf8],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here it used to accept anything

signature: Signature::coercible(
vec![
Coercion::new_implicit(
TypeSignatureClass::Native(logical_string()),
vec![TypeSignatureClass::Native(logical_binary())],
NativeType::String,
)
.with_encoding_preservation(EncodingPreservation::dictionary()),
],
Volatility::Immutable,
),
}
Expand All @@ -83,7 +91,9 @@ impl ScalarUDFImpl for ReverseFunc {
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
let args = &args.args;
match args[0].data_type() {
Utf8 | Utf8View | LargeUtf8 => make_scalar_function(reverse, vec![])(args),
Utf8 | Utf8View | LargeUtf8 | DataType::Dictionary(_, _) => {
make_scalar_function(reverse, vec![])(args)
}
other => {
exec_err!("Unsupported data type {other:?} for function reverse")
}
Expand Down Expand Up @@ -113,6 +123,11 @@ fn reverse(args: &[ArrayRef]) -> Result<ArrayRef> {
&args[0].as_string_view(),
StringViewArrayBuilder::with_capacity(len),
),
DataType::Dictionary(_, _) => {
let dictionary = args[0].as_any_dictionary();
let converted = reverse(&[Arc::clone(dictionary.values())])?;
Ok(dictionary.with_values(converted))
}
_ => unreachable!(
"Reverse can only be applied to Utf8View, Utf8 and LargeUtf8 types"
),
Expand Down
18 changes: 16 additions & 2 deletions datafusion/sqllogictest/test_files/binary.slt
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ SELECT
cast(binary as varchar) as str,
character_length(binary) as binary_len,
cast(largebinary as varchar) as large_str,
character_length(binary) as largebinary_len
character_length(largebinary) as largebinary_len
from t;
----
Foo 3 Foo 3
Expand All @@ -298,6 +298,20 @@ SELECT character_length(X'20');
query error Encountered non UTF\-8 data: invalid utf\-8 sequence of 1 bytes from index 0
SELECT character_length(X'c328');

# reverse function
query TTTT
SELECT
cast(binary as varchar) as str,
reverse(binary) as binary_reversed,
cast(largebinary as varchar) as large_str,
reverse(largebinary) as largebinary_reversed
from t;
----
Foo ooF Foo ooF
NULL NULL NULL NULL
Bar raB Bar raB
FooBar raBooF FooBar raBooF

# regexp_replace
query TTTT
SELECT
Expand Down Expand Up @@ -363,4 +377,4 @@ hellohello
query T
SELECT 'hello' || arrow_cast(arrow_cast('hello', 'Binary'), 'BinaryView');
----
hellohello
hellohello
Loading
Loading