Skip to content

Commit c11a73c

Browse files
committed
perf: preserve dictionary encoding in substr
For scalar start/length, substr can run over the dict's unique values and reuse the keys instead of casting the dict to its value type. - Signature uses EncodingPreservation::dictionary() so coercion keeps the dict wrapper. - Fast path in invoke_with_args runs substr on dict.values() and rewraps via with_values(..). - Per-row start/length still materializes the dict. - return_field_from_args declares Dictionary for the scalar case and the value type for the per-row case so the planner's expected schema matches runtime output. Closes #26170
1 parent 1c49b7f commit c11a73c

2 files changed

Lines changed: 97 additions & 8 deletions

File tree

‎datafusion/functions/src/unicode/substr.rs‎

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@ use arrow::array::{
2424
StringArrayType, StringViewArray,
2525
};
2626
use arrow::buffer::{NullBuffer, ScalarBuffer};
27-
use arrow::datatypes::DataType;
27+
use arrow::datatypes::{DataType, Field, FieldRef};
2828
use datafusion_common::cast::as_int64_array;
2929
use datafusion_common::types::{
3030
NativeType, logical_int32, logical_int64, logical_string,
3131
};
3232
use datafusion_common::{Result, exec_err};
3333
use datafusion_expr::{
34-
Coercion, ColumnarValue, Documentation, ScalarFunctionArgs, ScalarUDFImpl, Signature,
35-
TypeSignature, TypeSignatureClass, Volatility,
34+
Coercion, ColumnarValue, Documentation, EncodingPreservation, ReturnFieldArgs,
35+
ScalarFunctionArgs, ScalarUDFImpl, Signature, TypeSignature, TypeSignatureClass,
36+
Volatility,
3637
};
3738
use datafusion_macros::user_doc;
3839

@@ -73,7 +74,8 @@ impl Default for SubstrFunc {
7374

7475
impl SubstrFunc {
7576
pub fn new() -> Self {
76-
let string = Coercion::new_exact(TypeSignatureClass::Native(logical_string()));
77+
let string = Coercion::new_exact(TypeSignatureClass::Native(logical_string()))
78+
.with_encoding_preservation(EncodingPreservation::dictionary());
7779
let int64 = Coercion::new_implicit(
7880
TypeSignatureClass::Native(logical_int64()),
7981
vec![TypeSignatureClass::Native(logical_int32())],
@@ -115,8 +117,51 @@ impl ScalarUDFImpl for SubstrFunc {
115117
Ok(arg_types[0].clone())
116118
}
117119

120+
fn return_field_from_args(&self, args: ReturnFieldArgs) -> Result<FieldRef> {
121+
// Per-row start/length materializes the dict, so the output type is the
122+
// value type; scalar tail keeps the dict wrapper.
123+
let input = &args.arg_fields[0];
124+
let output_type = match input.data_type() {
125+
DataType::Dictionary(_, value_type)
126+
if !args.scalar_arguments.iter().skip(1).all(Option::is_some) =>
127+
{
128+
value_type.as_ref().clone()
129+
}
130+
other => other.clone(),
131+
};
132+
Ok(Field::new(self.name(), output_type, input.is_nullable()).into())
133+
}
134+
118135
fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
119-
make_scalar_function(substr, vec![])(&args.args)
136+
if let ColumnarValue::Array(array) = &args.args[0]
137+
&& matches!(array.data_type(), DataType::Dictionary(_, _))
138+
&& args.args[1..]
139+
.iter()
140+
.all(|v| matches!(v, ColumnarValue::Scalar(_)))
141+
{
142+
// Fast path: substr the N_distinct values, reuse the keys.
143+
let dict = array.as_any_dictionary();
144+
let values = Arc::clone(dict.values());
145+
let mut inner = Vec::with_capacity(args.args.len());
146+
inner.push(ColumnarValue::Array(values));
147+
inner.extend_from_slice(&args.args[1..]);
148+
let new_values = make_scalar_function(substr, vec![])(&inner)?
149+
.into_array(dict.values().len())?;
150+
return Ok(ColumnarValue::Array(dict.with_values(new_values)));
151+
}
152+
153+
// Per-row start/length: materialize the dict so substr can run row-wise.
154+
let args = if let ColumnarValue::Array(array) = &args.args[0]
155+
&& let DataType::Dictionary(_, value_type) = array.data_type()
156+
{
157+
let flat = arrow::compute::kernels::cast::cast(array, value_type.as_ref())?;
158+
let mut new_args = args.args.clone();
159+
new_args[0] = ColumnarValue::Array(flat);
160+
new_args
161+
} else {
162+
args.args
163+
};
164+
make_scalar_function(substr, vec![])(&args)
120165
}
121166

122167
fn aliases(&self) -> &[String] {
@@ -354,13 +399,15 @@ mod tests {
354399
use std::sync::Arc;
355400

356401
use arrow::array::{
357-
Array, ArrayRef, AsArray, Int64Array, LargeStringArray, StringArray,
358-
StringViewArray,
402+
Array, ArrayRef, AsArray, DictionaryArray, Int64Array, LargeStringArray,
403+
StringArray, StringViewArray,
359404
};
360405
use arrow::datatypes::DataType::{LargeUtf8, Utf8, Utf8View};
406+
use arrow::datatypes::{DataType, Field, Int32Type};
361407

408+
use datafusion_common::config::ConfigOptions;
362409
use datafusion_common::{Result, ScalarValue, exec_err};
363-
use datafusion_expr::{ColumnarValue, ScalarUDFImpl};
410+
use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl};
364411

365412
use crate::unicode::substr::SubstrFunc;
366413
use crate::utils::test::test_function;
@@ -766,4 +813,40 @@ mod tests {
766813

767814
Ok(())
768815
}
816+
817+
#[test]
818+
fn test_substr_dict_fast_path_preserves_encoding() -> Result<()> {
819+
let dict_type = DataType::Dictionary(Box::new(DataType::Int32), Box::new(Utf8));
820+
let keys = arrow::array::Int32Array::from(vec![0, 1, 0, 2, 1]);
821+
let values = StringArray::from(vec!["silver", "copper", "golden"]);
822+
let dict: ArrayRef = Arc::new(
823+
DictionaryArray::<Int32Type>::try_new(keys, Arc::new(values)).unwrap(),
824+
);
825+
826+
let result = SubstrFunc::new()
827+
.invoke_with_args(ScalarFunctionArgs {
828+
args: vec![
829+
ColumnarValue::Array(dict),
830+
ColumnarValue::Scalar(ScalarValue::from(1i64)),
831+
ColumnarValue::Scalar(ScalarValue::from(3i64)),
832+
],
833+
arg_fields: vec![
834+
Arc::new(Field::new("s", dict_type.clone(), true)),
835+
Arc::new(Field::new("start", DataType::Int64, false)),
836+
Arc::new(Field::new("len", DataType::Int64, false)),
837+
],
838+
number_rows: 5,
839+
return_field: Arc::new(Field::new("r", dict_type, true)),
840+
config_options: Arc::new(ConfigOptions::default()),
841+
})?
842+
.into_array(5)?;
843+
844+
assert!(matches!(result.data_type(), DataType::Dictionary(_, v) if **v == Utf8));
845+
let out = result.as_dictionary::<Int32Type>();
846+
let v = out.values().as_string::<i32>();
847+
assert_eq!(v.len(), 3);
848+
assert_eq!((v.value(0), v.value(1), v.value(2)), ("sil", "cop", "gol"));
849+
850+
Ok(())
851+
}
769852
}

‎datafusion/sqllogictest/test_files/string/dictionary_utf8.slt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ SELECT arrow_cast('', 'Dictionary(Int32, Utf8)');
5757
#
5858
include ./string_query.slt.part
5959

60+
# Scalar start/length preserves dict encoding end-to-end.
61+
query T
62+
SELECT DISTINCT arrow_typeof(substr(c1, 1, 3)) FROM test_substr;
63+
----
64+
Dictionary(Int32, Utf8)
65+
6066
#
6167
# Clean up
6268
#

0 commit comments

Comments
 (0)