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
128 changes: 120 additions & 8 deletions datafusion/functions/src/string/octet_length.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ use arrow::compute::kernels::length::length;
use arrow::datatypes::DataType;

use crate::utils::{transform_leaf_type_preserving_encoding, utf8_to_int_type};
use datafusion_common::types::logical_string;
use datafusion_common::types::{logical_binary, logical_string};
use datafusion_common::utils::take_function_args;
use datafusion_common::{Result, ScalarValue};
use datafusion_expr::{
Coercion, ColumnarValue, Documentation, EncodingPreservation, ScalarFunctionArgs,
ScalarUDFImpl, Signature, TypeSignatureClass, Volatility,
ScalarUDFImpl, Signature, TypeSignature, TypeSignatureClass, Volatility,
};
use datafusion_macros::user_doc;

Expand Down Expand Up @@ -58,10 +58,20 @@ impl Default for OctetLengthFunc {
impl OctetLengthFunc {
pub fn new() -> Self {
Self {
signature: Signature::coercible(
signature: Signature::one_of(
vec![
Coercion::new_exact(TypeSignatureClass::Native(logical_string()))
.with_encoding_preservation(EncodingPreservation::dictionary()),
TypeSignature::Coercible(vec![
Coercion::new_exact(TypeSignatureClass::Native(logical_string()))
.with_encoding_preservation(
EncodingPreservation::dictionary(),
),
]),
TypeSignature::Coercible(vec![
Coercion::new_exact(TypeSignatureClass::Native(logical_binary()))
.with_encoding_preservation(
EncodingPreservation::dictionary(),
),
]),
],
Volatility::Immutable,
),
Expand Down Expand Up @@ -107,6 +117,13 @@ fn octet_length_scalar(value: &ScalarValue) -> ScalarValue {
ScalarValue::Utf8View(v) => {
ScalarValue::Int32(v.as_ref().map(|x| x.len() as i32))
}
ScalarValue::Binary(v) => ScalarValue::Int32(v.as_ref().map(|x| x.len() as i32)),
ScalarValue::LargeBinary(v) => {
ScalarValue::Int64(v.as_ref().map(|x| x.len() as i64))
}
ScalarValue::BinaryView(v) => {
ScalarValue::Int32(v.as_ref().map(|x| x.len() as i32))
}
ScalarValue::Dictionary(key_type, value) => ScalarValue::Dictionary(
key_type.clone(),
Box::new(octet_length_scalar(value)),
Expand All @@ -119,8 +136,11 @@ fn octet_length_scalar(value: &ScalarValue) -> ScalarValue {
mod tests {
use std::sync::Arc;

use arrow::array::{Array, Int32Array, StringArray};
use arrow::datatypes::DataType::Int32;
use arrow::array::{
Array, BinaryArray, BinaryViewArray, Int32Array, Int64Array, LargeBinaryArray,
StringArray,
};
use arrow::datatypes::DataType::{Int32, Int64};

use datafusion_common::ScalarValue;
use datafusion_common::{Result, exec_err};
Expand All @@ -135,7 +155,7 @@ mod tests {
OctetLengthFunc::new(),
vec![ColumnarValue::Scalar(ScalarValue::Int32(Some(12)))],
exec_err!(
"The OCTET_LENGTH function can only accept strings, but got Int32."
"The OCTET_LENGTH function can only accept strings or binary, but got Int32."
),
i32,
Int32,
Expand Down Expand Up @@ -232,6 +252,98 @@ mod tests {
Int32Array
);

// Binary inputs: byte length, no string coercion.
test_function!(
OctetLengthFunc::new(),
vec![ColumnarValue::Array(Arc::new(BinaryArray::from(vec![
&b"chars"[..],
&b"chars2"[..],
])))],
Ok(Some(5)),
i32,
Int32,
Int32Array
);
test_function!(
OctetLengthFunc::new(),
vec![ColumnarValue::Scalar(ScalarValue::Binary(Some(
b"chars".to_vec()
)))],
Ok(Some(5)),
i32,
Int32,
Int32Array
);
// Arbitrary non-UTF-8 bytes: the case CAST(col AS VARCHAR) cannot serve.
test_function!(
OctetLengthFunc::new(),
vec![ColumnarValue::Scalar(ScalarValue::Binary(Some(vec![
0xff, 0xfe, 0x00, 0x80
])))],
Ok(Some(4)),
i32,
Int32,
Int32Array
);
test_function!(
OctetLengthFunc::new(),
vec![ColumnarValue::Scalar(ScalarValue::Binary(Some(vec![])))],
Ok(Some(0)),
i32,
Int32,
Int32Array
);
test_function!(
OctetLengthFunc::new(),
vec![ColumnarValue::Scalar(ScalarValue::Binary(None))],
Ok(None),
i32,
Int32,
Int32Array
);
test_function!(
OctetLengthFunc::new(),
vec![ColumnarValue::Array(Arc::new(BinaryViewArray::from(vec![
&b"chars"[..],
&b"chars2"[..],
])))],
Ok(Some(5)),
i32,
Int32,
Int32Array
);
test_function!(
OctetLengthFunc::new(),
vec![ColumnarValue::Scalar(ScalarValue::BinaryView(Some(
b"chars".to_vec()
)))],
Ok(Some(5)),
i32,
Int32,
Int32Array
);
// LargeBinary widens the return type to Int64, mirroring LargeUtf8.
test_function!(
OctetLengthFunc::new(),
vec![ColumnarValue::Array(Arc::new(LargeBinaryArray::from(
vec![&b"chars"[..], &b"chars2"[..]]
)))],
Ok(Some(5)),
i64,
Int64,
Int64Array
);
test_function!(
OctetLengthFunc::new(),
vec![ColumnarValue::Scalar(ScalarValue::LargeBinary(Some(
b"chars".to_vec()
)))],
Ok(Some(5)),
i64,
Int64,
Int64Array
);

Ok(())
}
}
4 changes: 2 additions & 2 deletions datafusion/functions/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ macro_rules! get_optimal_return_type {
DataType::Null => DataType::Null,
_ => {
return datafusion_common::exec_err!(
"The {} function can only accept strings, but got {:?}.",
"The {} function can only accept strings or binary, but got {:?}.",
name.to_uppercase(),
**value_type
);
}
},
data_type => {
return datafusion_common::exec_err!(
"The {} function can only accept strings, but got {:?}.",
"The {} function can only accept strings or binary, but got {:?}.",
name.to_uppercase(),
data_type
);
Expand Down
42 changes: 42 additions & 0 deletions datafusion/sqllogictest/test_files/functions.slt
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,48 @@ ORDER BY id
2 2 Dictionary(Int32, Int32) Dictionary(Int32, Dictionary(UInt32, Int32))
NULL NULL Dictionary(Int32, Int32) Dictionary(Int32, Dictionary(UInt32, Int32))

# octet_length over binary types: byte semantics, no string coercion
query I
SELECT octet_length(arrow_cast('foo', 'Binary'))
----
3

query IT
SELECT octet_length(arrow_cast('josé', 'Binary')),
arrow_typeof(octet_length(arrow_cast('josé', 'Binary')))
----
5 Int32

query IT
SELECT octet_length(arrow_cast('foo', 'BinaryView')),
arrow_typeof(octet_length(arrow_cast('foo', 'BinaryView')))
----
3 Int32

# LargeBinary widens the return type to Int64, mirroring LargeUtf8
query IT
SELECT octet_length(arrow_cast('foo', 'LargeBinary')),
arrow_typeof(octet_length(arrow_cast('foo', 'LargeBinary')))
----
3 Int64

query I
SELECT octet_length(arrow_cast(NULL, 'Binary'))
----
NULL

# Dictionary encoding is preserved over binary values too
query ?T
SELECT octet_length(arrow_cast(arrow_cast('foo', 'Binary'), 'Dictionary(Int32, Binary)')),
arrow_typeof(octet_length(arrow_cast(arrow_cast('foo', 'Binary'), 'Dictionary(Int32, Binary)')))
----
3 Dictionary(Int32, Int32)

# FixedSizeBinary is a distinct logical type and is deliberately not accepted,

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 would expect we support the FixedSizeBinary the same way its done for crc32

SELECT crc32(arrow_cast(arrow_cast('Spark', 'Binary'), 'FixedSizeBinary(5)'));

# matching md5 and the other Native(logical_binary()) signatures.
query error Function 'octet_length' failed to match any signature
SELECT octet_length(arrow_cast(arrow_cast('foo', 'Binary'), 'FixedSizeBinary(3)'))

query ??TT
SELECT character_length(dict_col), character_length(nested_dict_col),
arrow_typeof(character_length(dict_col)),
Expand Down