This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-23470-360a56dfe48edfb5b7391b911b75d22959e33c6a in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit f87e8174c107f61740606fa8c38b8f446911a1e8 Author: Andy Grove <[email protected]> AuthorDate: Sun Jul 12 13:35:39 2026 -0600 perf: optimize make_date in datafusion-functions (#23470) ## Which issue does this PR close? N/A ## Rationale for this change Optimize existing expression. ## What changes are included in this PR? Replaced make_date's per-element triple is_null check + PrimitiveBuilder with a single unioned NullBuffer, raw value-slice reads, and direct PrimitiveArray::new construction (matching the already-optimized make_time), cutting per-row overhead on the hot valid-input path. ## Are these changes tested? Existing tests + new tests. Benchmark (criterion): - make_date_scalar_col_col_8192: 19.979% faster (base 31210ns -> cand 24974ns) - make_date_col_col_col_8192: 16.931% faster (base 30849ns -> cand 25626ns) - make_date_scalar_scalar_col_8192: 22.999% faster (base 31551ns -> cand 24295ns) - make_date_scalar_scalar_scalar: 1.271% faster (base 48ns -> cand 48ns) ## Are there any user-facing changes? No <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. --> <!-- If there are any breaking changes to public APIs, please add the `api change` label. --> --- datafusion/functions/src/datetime/make_date.rs | 102 +++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/datafusion/functions/src/datetime/make_date.rs b/datafusion/functions/src/datetime/make_date.rs index dc1328742f..3a6b76ed86 100644 --- a/datafusion/functions/src/datetime/make_date.rs +++ b/datafusion/functions/src/datetime/make_date.rs @@ -17,10 +17,10 @@ use std::sync::Arc; -use arrow::array::builder::PrimitiveBuilder; use arrow::array::cast::AsArray; use arrow::array::types::{Date32Type, Int32Type}; use arrow::array::{Array, PrimitiveArray}; +use arrow::buffer::NullBuffer; use arrow::datatypes::DataType; use arrow::datatypes::DataType::Date32; use chrono::prelude::*; @@ -139,24 +139,27 @@ impl ScalarUDFImpl for MakeDateFunc { let months = months.as_primitive::<Int32Type>(); let days = days.as_primitive::<Int32Type>(); - let mut builder: PrimitiveBuilder<Date32Type> = - PrimitiveArray::builder(len); + let nulls = + NullBuffer::union_many([years.nulls(), months.nulls(), days.nulls()]); + let mut values = Vec::with_capacity(len); for i in 0..len { // match postgresql behaviour which returns null for any null input - if years.is_null(i) || months.is_null(i) || days.is_null(i) { - builder.append_null(); + if nulls.as_ref().is_some_and(|n| n.is_null(i)) { + values.push(0); } else { make_date_inner( years.value(i), months.value(i), days.value(i), - |days: i32| builder.append_value(days), + |days: i32| values.push(days), )?; } } - Ok(ColumnarValue::Array(Arc::new(builder.finish()))) + Ok(ColumnarValue::Array(Arc::new( + PrimitiveArray::<Date32Type>::new(values.into(), nulls), + ))) } } } @@ -197,3 +200,88 @@ fn make_date_inner<F: FnMut(i32)>( exec_err!("Unable to parse date from {year}, {month}, {day}") } } + +#[cfg(test)] +mod tests { + use super::*; + use arrow::array::Int32Array; + use arrow::datatypes::Field; + use datafusion_common::config::ConfigOptions; + + fn invoke(args: Vec<ColumnarValue>, number_rows: usize) -> Result<ColumnarValue> { + let arg_fields = args + .iter() + .map(|a| Field::new("a", a.data_type(), true).into()) + .collect::<Vec<_>>(); + MakeDateFunc::new().invoke_with_args(ScalarFunctionArgs { + args, + arg_fields, + number_rows, + return_field: Field::new("f", Date32, true).into(), + config_options: Arc::new(ConfigOptions::default()), + }) + } + + #[test] + fn test_make_date_array() { + let years = ColumnarValue::Array(Arc::new(Int32Array::from(vec![ + Some(1970), + Some(1970), + ]))); + let months = + ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(1), Some(1)]))); + let days = + ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(1), Some(2)]))); + + let ColumnarValue::Array(arr) = invoke(vec![years, months, days], 2).unwrap() + else { + panic!("expected array result"); + }; + let arr = arr.as_primitive::<Date32Type>(); + // Days since the unix epoch. + assert_eq!(arr.value(0), 0); + assert_eq!(arr.value(1), 1); + } + + #[test] + fn test_make_date_null_propagation() { + // A NULL in any component column yields a NULL row. + let years = + ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(2000), None]))); + let months = + ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(6), Some(6)]))); + let days = ColumnarValue::Array(Arc::new(Int32Array::from(vec![None, Some(15)]))); + + let ColumnarValue::Array(arr) = invoke(vec![years, months, days], 2).unwrap() + else { + panic!("expected array result"); + }; + let arr = arr.as_primitive::<Date32Type>(); + assert!(arr.is_null(0)); + assert!(arr.is_null(1)); + } + + #[test] + fn test_make_date_scalar_array_mix() { + let year = ColumnarValue::Scalar(ScalarValue::Int32(Some(1970))); + let month = ColumnarValue::Scalar(ScalarValue::Int32(Some(1))); + let days = + ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(1), Some(3)]))); + + let ColumnarValue::Array(arr) = invoke(vec![year, month, days], 2).unwrap() + else { + panic!("expected array result"); + }; + let arr = arr.as_primitive::<Date32Type>(); + assert_eq!(arr.value(0), 0); + assert_eq!(arr.value(1), 2); + } + + #[test] + fn test_make_date_out_of_range_errors() { + let years = ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(2000)]))); + let months = ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(13)]))); + let days = ColumnarValue::Array(Arc::new(Int32Array::from(vec![Some(1)]))); + assert!(invoke(vec![years, months, days], 1).is_err()); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
