From ad6241ebde1ff168f77c33acfc4e88808be35eeb Mon Sep 17 00:00:00 2001 From: Jamie Currier <247854506+StealthEyeLLC@users.noreply.github.com> Date: Thu, 9 Jul 2026 04:57:15 +0000 Subject: [PATCH 01/41] fix: avoid dictionary key overflow in group value output --- .../src/aggregates/group_values/mod.rs | 61 ++++++++++++++++++- .../src/aggregates/group_values/row.rs | 57 ++++++++++++++++- .../physical-plan/src/aggregates/mod.rs | 30 ++++++++- 3 files changed, 144 insertions(+), 4 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/group_values/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/mod.rs index ee253e5d7afdd..99823ae8b774b 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/mod.rs @@ -23,11 +23,70 @@ use arrow::array::types::{ TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType, }; use arrow::array::{ArrayRef, downcast_primitive}; -use arrow::datatypes::{DataType, SchemaRef, TimeUnit}; +use arrow::datatypes::{DataType, Field, FieldRef, Schema, SchemaRef, TimeUnit}; use datafusion_common::Result; +use std::sync::Arc; use datafusion_expr::EmitTo; +pub(crate) fn group_value_output_schema(schema: &Schema) -> SchemaRef { + Arc::new(Schema::new( + schema + .fields() + .iter() + .map(|field| group_value_output_field(field.as_ref())) + .collect::>(), + )) +} + +pub(crate) fn group_value_output_field(field: &Field) -> FieldRef { + Arc::new( + Field::new( + field.name(), + group_value_output_data_type(field.data_type()), + field.is_nullable(), + ) + .with_metadata(field.metadata().clone()), + ) +} + +fn group_value_output_data_type(data_type: &DataType) -> DataType { + match data_type { + DataType::Dictionary(_, value_type) => DataType::Dictionary( + Box::new(DataType::UInt64), + Box::new(group_value_output_data_type(value_type)), + ), + DataType::Struct(fields) => DataType::Struct( + fields + .iter() + .map(|field| group_value_output_field(field.as_ref())) + .collect::>() + .into(), + ), + DataType::List(field) => DataType::List(group_value_output_field(field.as_ref())), + DataType::LargeList(field) => { + DataType::LargeList(group_value_output_field(field.as_ref())) + } + DataType::ListView(field) => { + DataType::ListView(group_value_output_field(field.as_ref())) + } + DataType::LargeListView(field) => { + DataType::LargeListView(group_value_output_field(field.as_ref())) + } + DataType::FixedSizeList(field, size) => { + DataType::FixedSizeList(group_value_output_field(field.as_ref()), *size) + } + DataType::Map(field, sorted) => { + DataType::Map(group_value_output_field(field.as_ref()), *sorted) + } + DataType::RunEndEncoded(run_ends, values) => DataType::RunEndEncoded( + Arc::clone(run_ends), + group_value_output_field(values.as_ref()), + ), + _ => data_type.clone(), + } +} + pub mod multi_group_by; mod row; diff --git a/datafusion/physical-plan/src/aggregates/group_values/row.rs b/datafusion/physical-plan/src/aggregates/group_values/row.rs index 4976a098ecee5..61cc21c801708 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/row.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/row.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use crate::aggregates::group_values::GroupValues; +use crate::aggregates::group_values::{GroupValues, group_value_output_schema}; use arrow::array::{ Array, ArrayRef, ListArray, PrimitiveArray, RunArray, StructArray, downcast_run_end_index, @@ -83,6 +83,7 @@ pub struct GroupValuesRows { } impl GroupValuesRows { + #[expect(clippy::needless_pass_by_value)] pub fn try_new(schema: SchemaRef) -> Result { // Print a debugging message, so it is clear when the (slower) fallback // GroupValuesRows is used. @@ -102,8 +103,9 @@ impl GroupValuesRows { let starting_data_capacity = 64 * starting_rows_capacity; let rows_buffer = row_converter.empty_rows(starting_rows_capacity, starting_data_capacity); + let output_schema = group_value_output_schema(schema.as_ref()); Ok(Self { - schema, + schema: output_schema, row_converter, map, map_size: 0, @@ -332,3 +334,54 @@ fn dictionary_encode_if_necessary( (_, _) => Ok(Arc::::clone(array)), } } + +#[cfg(test)] +mod tests { + use super::*; + use arrow::array::{DictionaryArray, StringArray, StringDictionaryBuilder}; + use arrow::datatypes::{Field, Schema, UInt8Type, UInt64Type}; + + #[test] + fn dict_uint8_utf8_257_groups_emit_promotes_key_type() -> Result<()> { + let schema = Arc::new(Schema::new(vec![Field::new( + "k", + DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)), + false, + )])); + + let mut group_values = GroupValuesRows::try_new(Arc::clone(&schema))?; + let mut groups = vec![]; + for i in 0u32..257 { + let mut builder = StringDictionaryBuilder::::new(); + builder.append_value(format!("group_{i}")); + let array: ArrayRef = Arc::new(builder.finish()); + group_values.intern(&[array], &mut groups)?; + } + + assert_eq!(group_values.len(), 257); + let arrays = group_values.emit(EmitTo::All)?; + assert_eq!(arrays.len(), 1); + assert_eq!(arrays[0].len(), 257); + assert_eq!( + arrays[0].data_type(), + &DataType::Dictionary(Box::new(DataType::UInt64), Box::new(DataType::Utf8)) + ); + + let dictionary = arrays[0] + .as_any() + .downcast_ref::>() + .unwrap(); + let values = dictionary + .values() + .as_any() + .downcast_ref::() + .unwrap(); + + for row in 0..257 { + let key = dictionary.keys().value(row) as usize; + assert_eq!(values.value(key), format!("group_{row}")); + } + + Ok(()) + } +} diff --git a/datafusion/physical-plan/src/aggregates/mod.rs b/datafusion/physical-plan/src/aggregates/mod.rs index d7c72253ecc0c..9ee06e92e3b11 100644 --- a/datafusion/physical-plan/src/aggregates/mod.rs +++ b/datafusion/physical-plan/src/aggregates/mod.rs @@ -465,7 +465,10 @@ impl PhysicalGroupBy { fn output_fields(&self, input_schema: &Schema) -> Result> { let mut fields = self.group_fields(input_schema)?; fields.truncate(self.num_output_exprs()); - Ok(fields) + Ok(fields + .iter() + .map(|field| group_values::group_value_output_field(field.as_ref())) + .collect()) } /// Returns the `PhysicalGroupBy` for a final aggregation if `self` is used for a partial @@ -7016,4 +7019,29 @@ mod tests { assert!(agg.with_dynamic_filter_expr(df).is_err()); Ok(()) } + + #[test] + fn aggregate_output_schema_widens_dictionary_group_key_type() -> Result<()> { + let schema = Arc::new(Schema::new(vec![Field::new( + "k", + DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)), + false, + )])); + let group_by = + PhysicalGroupBy::new_single(vec![(col("k", &schema)?, "k".to_string())]); + + let group_schema = group_by.group_schema(&schema)?; + assert_eq!( + group_schema.field(0).data_type(), + &DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)) + ); + + let output_schema = create_schema(&schema, &group_by, &[], AggregateMode::Final)?; + assert_eq!( + output_schema.field(0).data_type(), + &DataType::Dictionary(Box::new(DataType::UInt64), Box::new(DataType::Utf8)) + ); + + Ok(()) + } } From 711433c9cc6e0be0942f2fbcdb5202c4fe020d04 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 11:48:01 -0400 Subject: [PATCH 02/41] fix: support runtime dictionary key promotion --- .../src/aggregates/group_values/mod.rs | 131 ++++++++++++++---- 1 file changed, 104 insertions(+), 27 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/group_values/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/mod.rs index 99823ae8b774b..33985d7ca142a 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/mod.rs @@ -23,65 +23,142 @@ use arrow::array::types::{ TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType, }; use arrow::array::{ArrayRef, downcast_primitive}; +use arrow::compute::cast; use arrow::datatypes::{DataType, Field, FieldRef, Schema, SchemaRef, TimeUnit}; use datafusion_common::Result; use std::sync::Arc; use datafusion_expr::EmitTo; -pub(crate) fn group_value_output_schema(schema: &Schema) -> SchemaRef { - Arc::new(Schema::new( - schema - .fields() - .iter() - .map(|field| group_value_output_field(field.as_ref())) - .collect::>(), +/// Returns `schema` with its leading group fields updated to the data types +/// actually emitted by [`GroupValues`]. Dictionary key types may grow at +/// runtime, while all field names, nullability, and metadata remain unchanged. +pub(crate) fn schema_with_group_values( + schema: &SchemaRef, + group_values: &[ArrayRef], +) -> SchemaRef { + if group_values + .iter() + .zip(schema.fields()) + .all(|(array, field)| array.data_type() == field.data_type()) + { + return Arc::clone(schema); + } + + let fields = schema + .fields() + .iter() + .enumerate() + .map(|(index, field)| match group_values.get(index) { + Some(array) if array.data_type() != field.data_type() => Arc::new( + field + .as_ref() + .clone() + .with_data_type(array.data_type().clone()), + ), + _ => Arc::clone(field), + }) + .collect::>(); + + Arc::new(Schema::new_with_metadata( + fields, + schema.metadata().clone(), )) } -pub(crate) fn group_value_output_field(field: &Field) -> FieldRef { +/// Casts the leading group-value arrays to the corresponding schema fields. +/// This is used by spill files, which require one stable IPC schema even though +/// normal aggregate output may promote dictionary keys dynamically. +pub(crate) fn cast_group_values_to_schema( + group_values: &mut [ArrayRef], + schema: &SchemaRef, +) -> Result<()> { + for (array, field) in group_values.iter_mut().zip(schema.fields()) { + if array.data_type() != field.data_type() { + *array = cast(array.as_ref(), field.data_type())?; + } + } + Ok(()) +} + +/// Returns a stable spill schema whose dictionary group keys use their widest +/// key type. The widening is internal to spill IPC and does not change planned +/// or non-spill aggregate output schemas. +pub(crate) fn group_value_spill_schema( + schema: &SchemaRef, + num_group_fields: usize, +) -> SchemaRef { + let fields = schema + .fields() + .iter() + .enumerate() + .map(|(index, field)| { + if index < num_group_fields { + group_value_spill_field(field.as_ref()) + } else { + Arc::clone(field) + } + }) + .collect::>(); + + Arc::new(Schema::new_with_metadata( + fields, + schema.metadata().clone(), + )) +} + +fn group_value_spill_field(field: &Field) -> FieldRef { Arc::new( - Field::new( - field.name(), - group_value_output_data_type(field.data_type()), - field.is_nullable(), - ) - .with_metadata(field.metadata().clone()), + field + .clone() + .with_data_type(group_value_spill_data_type(field.data_type())), ) } -fn group_value_output_data_type(data_type: &DataType) -> DataType { +fn group_value_spill_data_type(data_type: &DataType) -> DataType { match data_type { - DataType::Dictionary(_, value_type) => DataType::Dictionary( - Box::new(DataType::UInt64), - Box::new(group_value_output_data_type(value_type)), - ), + DataType::Dictionary(key_type, value_type) => { + let key_type = match key_type.as_ref() { + DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => { + DataType::Int64 + } + DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 => DataType::UInt64, + _ => key_type.as_ref().clone(), + }; + DataType::Dictionary( + Box::new(key_type), + Box::new(group_value_spill_data_type(value_type)), + ) + } DataType::Struct(fields) => DataType::Struct( fields .iter() - .map(|field| group_value_output_field(field.as_ref())) + .map(|field| group_value_spill_field(field.as_ref())) .collect::>() .into(), ), - DataType::List(field) => DataType::List(group_value_output_field(field.as_ref())), + DataType::List(field) => DataType::List(group_value_spill_field(field.as_ref())), DataType::LargeList(field) => { - DataType::LargeList(group_value_output_field(field.as_ref())) + DataType::LargeList(group_value_spill_field(field.as_ref())) } DataType::ListView(field) => { - DataType::ListView(group_value_output_field(field.as_ref())) + DataType::ListView(group_value_spill_field(field.as_ref())) } DataType::LargeListView(field) => { - DataType::LargeListView(group_value_output_field(field.as_ref())) + DataType::LargeListView(group_value_spill_field(field.as_ref())) } DataType::FixedSizeList(field, size) => { - DataType::FixedSizeList(group_value_output_field(field.as_ref()), *size) + DataType::FixedSizeList(group_value_spill_field(field.as_ref()), *size) } DataType::Map(field, sorted) => { - DataType::Map(group_value_output_field(field.as_ref()), *sorted) + DataType::Map(group_value_spill_field(field.as_ref()), *sorted) } DataType::RunEndEncoded(run_ends, values) => DataType::RunEndEncoded( Arc::clone(run_ends), - group_value_output_field(values.as_ref()), + group_value_spill_field(values.as_ref()), ), _ => data_type.clone(), } From 6a133e50e8073673ccf11ccc3a998f4e99207de7 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 11:50:58 -0400 Subject: [PATCH 03/41] fix: promote dictionary group keys on emit --- .../src/aggregates/group_values/row.rs | 220 +++++++++++++++--- 1 file changed, 187 insertions(+), 33 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/group_values/row.rs b/datafusion/physical-plan/src/aggregates/group_values/row.rs index 61cc21c801708..a149e46ca6bc4 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/row.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/row.rs @@ -15,13 +15,14 @@ // specific language governing permissions and limitations // under the License. -use crate::aggregates::group_values::{GroupValues, group_value_output_schema}; +use crate::aggregates::group_values::{GroupValues, schema_with_group_values}; use arrow::array::{ Array, ArrayRef, ListArray, PrimitiveArray, RunArray, StructArray, downcast_run_end_index, }; use arrow::compute::cast; -use arrow::datatypes::{DataType, SchemaRef}; +use arrow::datatypes::{DataType, FieldRef, Schema, SchemaRef}; +use arrow::error::ArrowError; use arrow::row::{RowConverter, Rows, SortField}; use datafusion_common::Result; use datafusion_common::hash_utils::RandomState; @@ -43,9 +44,14 @@ use std::sync::Arc; /// It uses the arrow-rs [`Rows`] to store the group values, which is a row-wise /// representation. pub struct GroupValuesRows { - /// The output schema + /// The current output schema. Dictionary key types may grow, but never shrink. schema: SchemaRef, + /// Schema used by the row converter. Top-level dictionaries are materialized + /// to their value type so input batches with promoted dictionary keys remain + /// compatible with the same converter. + row_schema: SchemaRef, + /// Converter for the group values row_converter: RowConverter, @@ -83,13 +89,13 @@ pub struct GroupValuesRows { } impl GroupValuesRows { - #[expect(clippy::needless_pass_by_value)] pub fn try_new(schema: SchemaRef) -> Result { // Print a debugging message, so it is clear when the (slower) fallback // GroupValuesRows is used. debug!("Creating GroupValuesRows for schema: {schema}"); + let row_schema = materialized_row_schema(&schema); let row_converter = RowConverter::new( - schema + row_schema .fields() .iter() .map(|f| SortField::new(f.data_type().clone())) @@ -103,9 +109,9 @@ impl GroupValuesRows { let starting_data_capacity = 64 * starting_rows_capacity; let rows_buffer = row_converter.empty_rows(starting_rows_capacity, starting_data_capacity); - let output_schema = group_value_output_schema(schema.as_ref()); Ok(Self { - schema: output_schema, + schema, + row_schema, row_converter, map, map_size: 0, @@ -119,11 +125,23 @@ impl GroupValuesRows { impl GroupValues for GroupValuesRows { fn intern(&mut self, cols: &[ArrayRef], groups: &mut Vec) -> Result<()> { - // Normalize -0.0 → +0.0 so RowConverter (IEEE 754 totalOrder) and - // primitive hashing both group ±0 together. No-op for non-float - // columns. - let normalized_cols: Vec = - cols.iter().map(normalize_float_zero).collect(); + // Dictionary keys are an encoding detail and can be promoted between + // aggregate stages. Materialize top-level dictionaries before row + // conversion so the stored row representation remains stable. + // Also normalize -0.0 → +0.0 so RowConverter (IEEE 754 totalOrder) and + // primitive hashing both group ±0 together. + let normalized_cols = cols + .iter() + .zip(self.row_schema.fields()) + .map(|(col, field)| { + let col = if col.data_type() == field.data_type() { + Arc::clone(col) + } else { + cast(col.as_ref(), field.data_type())? + }; + Ok(normalize_float_zero(&col)) + }) + .collect::>>()?; let cols = normalized_cols.as_slice(); // Convert the group keys into the row format @@ -245,12 +263,13 @@ impl GroupValues for GroupValuesRows { } }; - // TODO: Materialize dictionaries in group keys - // https://github.com/apache/datafusion/issues/7647 - for (field, array) in self.schema.fields.iter().zip(&mut output) { - let expected = field.data_type(); - *array = dictionary_encode_if_necessary(array, expected)?; + // Re-encode dictionary group keys using the current key type. If the + // emitted cardinality no longer fits, grow to the next key width and + // retain that promoted type for all later emissions. + for (field, array) in self.schema.fields().iter().zip(&mut output) { + *array = dictionary_encode_if_necessary(array, field.data_type())?; } + self.schema = schema_with_group_values(&self.schema, &output); self.group_values = Some(group_values); Ok(output) @@ -269,6 +288,26 @@ impl GroupValues for GroupValuesRows { } } +fn materialized_row_schema(schema: &SchemaRef) -> SchemaRef { + let fields = schema + .fields() + .iter() + .map(|field| match field.data_type() { + DataType::Dictionary(_, value_type) => Arc::new( + field + .as_ref() + .clone() + .with_data_type(value_type.as_ref().clone()), + ), + _ => Arc::clone(field), + }) + .collect::>(); + Arc::new(Schema::new_with_metadata( + fields, + schema.metadata().clone(), + )) +} + fn dictionary_encode_if_necessary( array: &ArrayRef, expected: &DataType, @@ -283,27 +322,49 @@ fn dictionary_encode_if_necessary( dictionary_encode_if_necessary(column, expected_field.data_type()) }) .collect::>>()?; + let fields = expected_fields + .iter() + .zip(&arrays) + .map(|(field, array)| field_with_array_type(field, array)) + .collect::>() + .into(); Ok(Arc::new(StructArray::try_new( - expected_fields.clone(), + fields, arrays, struct_array.nulls().cloned(), )?)) } (DataType::List(expected_field), &DataType::List(_)) => { let list = array.as_any().downcast_ref::().unwrap(); + let values = dictionary_encode_if_necessary( + list.values(), + expected_field.data_type(), + )?; + let field = field_with_array_type(expected_field, &values); Ok(Arc::new(ListArray::try_new( - Arc::::clone(expected_field), + field, list.offsets().clone(), - dictionary_encode_if_necessary( - list.values(), - expected_field.data_type(), - )?, + values, list.nulls().cloned(), )?)) } - (DataType::Dictionary(_, _), _) => Ok(cast(array.as_ref(), expected)?), + (DataType::Dictionary(_, _), _) => { + let mut target = expected.clone(); + loop { + match cast(array.as_ref(), &target) { + Ok(array) => return Ok(array), + Err(ArrowError::DictionaryKeyOverflowError) => { + let Some(promoted) = promote_dictionary_type(&target) else { + return Err(ArrowError::DictionaryKeyOverflowError.into()); + }; + target = promoted; + } + Err(error) => return Err(error.into()), + } + } + } ( DataType::RunEndEncoded(run_ends_field, expected_values_field), &DataType::RunEndEncoded(_, _), @@ -335,14 +396,49 @@ fn dictionary_encode_if_necessary( } } +fn field_with_array_type(field: &FieldRef, array: &ArrayRef) -> FieldRef { + if field.data_type() == array.data_type() { + Arc::clone(field) + } else { + Arc::new( + field + .as_ref() + .clone() + .with_data_type(array.data_type().clone()), + ) + } +} + +fn promote_dictionary_type(data_type: &DataType) -> Option { + let DataType::Dictionary(key_type, value_type) = data_type else { + return None; + }; + let key_type = match key_type.as_ref() { + DataType::Int8 => DataType::Int16, + DataType::Int16 => DataType::Int32, + DataType::Int32 => DataType::Int64, + DataType::UInt8 => DataType::UInt16, + DataType::UInt16 => DataType::UInt32, + DataType::UInt32 => DataType::UInt64, + DataType::Int64 | DataType::UInt64 => return None, + _ => return None, + }; + Some(DataType::Dictionary( + Box::new(key_type), + Box::new(value_type.as_ref().clone()), + )) +} + #[cfg(test)] mod tests { use super::*; use arrow::array::{DictionaryArray, StringArray, StringDictionaryBuilder}; - use arrow::datatypes::{Field, Schema, UInt8Type, UInt64Type}; + use arrow::datatypes::{ + Field, Int8Type, Int16Type, Schema, UInt8Type, UInt16Type, + }; #[test] - fn dict_uint8_utf8_257_groups_emit_promotes_key_type() -> Result<()> { + fn dict_uint8_utf8_promotes_and_does_not_shrink() -> Result<()> { let schema = Arc::new(Schema::new(vec![Field::new( "k", DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)), @@ -358,30 +454,88 @@ mod tests { group_values.intern(&[array], &mut groups)?; } - assert_eq!(group_values.len(), 257); let arrays = group_values.emit(EmitTo::All)?; - assert_eq!(arrays.len(), 1); - assert_eq!(arrays[0].len(), 257); assert_eq!( arrays[0].data_type(), - &DataType::Dictionary(Box::new(DataType::UInt64), Box::new(DataType::Utf8)) + &DataType::Dictionary(Box::new(DataType::UInt16), Box::new(DataType::Utf8)) ); - let dictionary = arrays[0] .as_any() - .downcast_ref::>() + .downcast_ref::>() .unwrap(); let values = dictionary .values() .as_any() .downcast_ref::() .unwrap(); - for row in 0..257 { let key = dictionary.keys().value(row) as usize; assert_eq!(values.value(key), format!("group_{row}")); } + let mut builder = StringDictionaryBuilder::::new(); + builder.append_value("after_promotion"); + group_values.intern(&[Arc::new(builder.finish())], &mut groups)?; + let arrays = group_values.emit(EmitTo::All)?; + assert_eq!( + arrays[0].data_type(), + &DataType::Dictionary(Box::new(DataType::UInt16), Box::new(DataType::Utf8)) + ); + + Ok(()) + } + + #[test] + fn dict_uint8_utf8_keeps_key_type_at_capacity() -> Result<()> { + let schema = Arc::new(Schema::new(vec![Field::new( + "k", + DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)), + false, + )])); + let mut group_values = GroupValuesRows::try_new(schema)?; + let mut groups = vec![]; + for i in 0u32..256 { + let mut builder = StringDictionaryBuilder::::new(); + builder.append_value(format!("group_{i}")); + group_values.intern(&[Arc::new(builder.finish())], &mut groups)?; + } + + let arrays = group_values.emit(EmitTo::All)?; + assert_eq!( + arrays[0].data_type(), + &DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)) + ); + assert!(arrays[0] + .as_any() + .downcast_ref::>() + .is_some()); + Ok(()) + } + + #[test] + fn dict_int8_utf8_promotes_after_capacity() -> Result<()> { + let schema = Arc::new(Schema::new(vec![Field::new( + "k", + DataType::Dictionary(Box::new(DataType::Int8), Box::new(DataType::Utf8)), + false, + )])); + let mut group_values = GroupValuesRows::try_new(schema)?; + let mut groups = vec![]; + for i in 0u32..129 { + let mut builder = StringDictionaryBuilder::::new(); + builder.append_value(format!("group_{i}")); + group_values.intern(&[Arc::new(builder.finish())], &mut groups)?; + } + + let arrays = group_values.emit(EmitTo::All)?; + assert_eq!( + arrays[0].data_type(), + &DataType::Dictionary(Box::new(DataType::Int16), Box::new(DataType::Utf8)) + ); + assert!(arrays[0] + .as_any() + .downcast_ref::>() + .is_some()); Ok(()) } } From ca2a9e05e18ac18b11805019cca190966b2bd3f3 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 11:51:23 -0400 Subject: [PATCH 04/41] fix: keep planned aggregate dictionary schema unchanged --- .../physical-plan/src/aggregates/mod.rs | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/mod.rs b/datafusion/physical-plan/src/aggregates/mod.rs index 9ee06e92e3b11..d7c72253ecc0c 100644 --- a/datafusion/physical-plan/src/aggregates/mod.rs +++ b/datafusion/physical-plan/src/aggregates/mod.rs @@ -465,10 +465,7 @@ impl PhysicalGroupBy { fn output_fields(&self, input_schema: &Schema) -> Result> { let mut fields = self.group_fields(input_schema)?; fields.truncate(self.num_output_exprs()); - Ok(fields - .iter() - .map(|field| group_values::group_value_output_field(field.as_ref())) - .collect()) + Ok(fields) } /// Returns the `PhysicalGroupBy` for a final aggregation if `self` is used for a partial @@ -7019,29 +7016,4 @@ mod tests { assert!(agg.with_dynamic_filter_expr(df).is_err()); Ok(()) } - - #[test] - fn aggregate_output_schema_widens_dictionary_group_key_type() -> Result<()> { - let schema = Arc::new(Schema::new(vec![Field::new( - "k", - DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)), - false, - )])); - let group_by = - PhysicalGroupBy::new_single(vec![(col("k", &schema)?, "k".to_string())]); - - let group_schema = group_by.group_schema(&schema)?; - assert_eq!( - group_schema.field(0).data_type(), - &DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)) - ); - - let output_schema = create_schema(&schema, &group_by, &[], AggregateMode::Final)?; - assert_eq!( - output_schema.field(0).data_type(), - &DataType::Dictionary(Box::new(DataType::UInt64), Box::new(DataType::Utf8)) - ); - - Ok(()) - } } From 3b556fa653ea181dd6b347c5dfa71e832d0e2f61 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 11:54:12 -0400 Subject: [PATCH 05/41] fix: use emitted group schema in hash aggregate output --- .../src/aggregates/aggregate_hash_table/common.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs index e6e690c4d1e08..7c7c2cba836c3 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs @@ -27,7 +27,9 @@ use datafusion_expr::{EmitTo, GroupsAccumulator}; use datafusion_physical_expr::aggregate::AggregateFunctionExpr; use crate::PhysicalExpr; -use crate::aggregates::group_values::{GroupByMetrics, GroupValues, new_group_values}; +use crate::aggregates::group_values::{ + GroupByMetrics, GroupValues, new_group_values, schema_with_group_values, +}; use crate::aggregates::grouped_hash_stream::create_group_accumulator; use crate::aggregates::order::GroupOrdering; use crate::aggregates::{ @@ -233,11 +235,16 @@ impl AggregateHashTable { let emit_to = EmitTo::All; let timer = self.group_by_metrics.emitting_time.timer(); let mut columns = state.group_values.emit(emit_to)?; + let num_group_columns = columns.len(); for acc in state.accumulators.iter_mut() { columns.extend(materialize_accumulator_fn(acc, emit_to)?); } drop(timer); + let output_schema = schema_with_group_values( + &output_schema, + &columns[..num_group_columns], + ); let batch = RecordBatch::try_new(output_schema, columns)?; debug_assert!(batch.num_rows() > 0); MaterializedAggregateOutput::new(batch) @@ -336,7 +343,7 @@ pub(super) type AggregateAccumulator = HashAggregateAccumulator; /// Arguments: /// * accumulator to update. /// * accumulator's evaluated arguments and optional filter. -/// * one group index per input row, mapping each row to its interned group. +/// * group index per input row, mapping each row to its interned group. /// * total number of groups currently interned in that buffer, including newly /// interned groups. pub(super) type AggregateBatchFn = fn( From 2bf64ac3b876071c4910a004a39df76f5a307c0a Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 11:54:47 -0400 Subject: [PATCH 06/41] fix: use emitted group schema in ordered aggregate output --- .../aggregate_hash_table/common_ordered.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs index c83303c51d6e8..ec6d0c066d701 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs @@ -30,7 +30,9 @@ use datafusion_expr::EmitTo; use crate::InputOrderMode; use crate::PhysicalExpr; -use crate::aggregates::group_values::{GroupByMetrics, GroupValues, new_group_values}; +use crate::aggregates::group_values::{ + GroupByMetrics, GroupValues, new_group_values, schema_with_group_values, +}; use crate::aggregates::grouped_hash_stream::create_group_accumulator; use crate::aggregates::order::GroupOrdering; use crate::aggregates::{ @@ -253,7 +255,7 @@ impl OrderedAggregateTable { } /// Aggregates one evaluated input batch. /// - /// This common utility is used by ordered partial and ordered final aggregation. + /// This common utility is used by ordered partial and final aggregation. /// /// # Argument: `is_final` /// @@ -308,7 +310,7 @@ impl OrderedAggregateTable { /// Emits groups allowed by `GroupOrdering`, leaving only the current /// unfinished ordered-key range buffered. /// - /// This common utility is used by ordered partial and ordered final aggregation. + /// This common utility is used by ordered partial and final aggregation. /// /// # Argument: `is_final` /// @@ -330,6 +332,7 @@ impl OrderedAggregateTable { let timer = self.group_by_metrics.emitting_time.timer(); let mut output = self.buffer.group_values.emit(emit_to)?; + let num_group_columns = output.len(); if should_remove_groups { match emit_to { EmitTo::First(n) => self.buffer.group_ordering.remove_groups(n), @@ -349,7 +352,11 @@ impl OrderedAggregateTable { } drop(timer); - let batch = RecordBatch::try_new(Arc::clone(&self.output_schema), output)?; + let output_schema = schema_with_group_values( + &self.output_schema, + &output[..num_group_columns], + ); + let batch = RecordBatch::try_new(output_schema, output)?; debug_assert!(batch.num_rows() > 0); Ok(Some(batch)) From aaf35effc0522c8684a2101602649aa5cdca706e Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 11:59:24 -0400 Subject: [PATCH 07/41] fix: propagate promoted dictionary schemas through legacy aggregation --- .../src/aggregates/grouped_hash_stream.rs | 96 ++++++++++++++++--- 1 file changed, 81 insertions(+), 15 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs b/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs index 0d00e5c4d0d86..3df5c4aceb305 100644 --- a/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs +++ b/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs @@ -24,7 +24,10 @@ use std::vec; use super::order::GroupOrdering; use super::skip_partial::SkipAggregationProbe; use super::{AggregateExec, format_human_display}; -use crate::aggregates::group_values::{GroupByMetrics, GroupValues, new_group_values}; +use crate::aggregates::group_values::{ + GroupByMetrics, GroupValues, cast_group_values_to_schema, + group_value_spill_schema, new_group_values, schema_with_group_values, +}; use crate::aggregates::order::GroupOrderingFull; use crate::aggregates::{ AggregateInputMode, AggregateMode, AggregateOutputMode, PhysicalGroupBy, @@ -424,6 +427,7 @@ impl GroupedHashAggregateStream { .collect::>()?; let group_schema = agg_group_by.group_schema(&agg.input().schema())?; + let num_group_fields = group_schema.fields().len(); // fix https://github.com/apache/datafusion/issues/13949 // Builds a **partial aggregation** schema by combining the group columns and @@ -446,6 +450,7 @@ impl GroupedHashAggregateStream { &aggregate_exprs, AggregateMode::Partial, )?); + let spill_schema = group_value_spill_schema(&spill_schema, num_group_fields); // Need to update the GROUP BY expressions to point to the correct column after schema change let merging_group_by_expr = agg_group_by @@ -760,7 +765,7 @@ impl Stream for GroupedHashAggregateStream { if !self.group_values.is_empty() { return Poll::Ready(Some(internal_err!( "Switching from SkippingAggregation to Done with {} groups still in hash table. \ - This is a bug - all groups should have been emitted before skip aggregation started.", +This is a bug - all groups should have been emitted before skip aggregation started.", self.group_values.len() ))); } @@ -819,7 +824,7 @@ impl Stream for GroupedHashAggregateStream { if !self.group_values.is_empty() { return Poll::Ready(Some(internal_err!( "AggregateStream was in Done state with {} groups left in hash table. \ - This is a bug - all groups should have been emitted before entering Done state.", +This is a bug - all groups should have been emitted before entering Done state.", self.group_values.len() ))); } @@ -1023,17 +1028,27 @@ impl GroupedHashAggregateStream { /// Create an output RecordBatch with the group keys and /// accumulator states/values specified in emit_to fn emit(&mut self, emit_to: EmitTo, spilling: bool) -> Result> { - let schema = if spilling { - Arc::clone(&self.spill_state.spill_schema) - } else { - self.schema() - }; if self.group_values.is_empty() { return Ok(None); } let timer = self.group_by_metrics.emitting_time.timer(); let mut output = self.group_values.emit(emit_to)?; + let num_group_columns = output.len(); + let schema = if spilling { + cast_group_values_to_schema( + &mut output[..num_group_columns], + &self.spill_state.spill_schema, + )?; + Arc::clone(&self.spill_state.spill_schema) + } else { + let schema = schema_with_group_values( + &self.schema, + &output[..num_group_columns], + ); + self.schema = Arc::clone(&schema); + schema + }; if let EmitTo::First(n) = emit_to { self.group_ordering.remove_groups(n); } @@ -1312,10 +1327,7 @@ impl GroupedHashAggregateStream { // in first-seen order, as required by `GroupOrderingFull`. // The pre-spill multi-column collector may use `vectorized_intern`, which // can assign new group ids out of input order under hash collisions. - let group_schema = self - .spill_state - .merging_group_by - .group_schema(&self.spill_state.spill_schema)?; + let group_schema = self.group_by.group_schema(&self.input_schema)?; if group_schema.fields().len() > 1 { self.group_values = new_group_values(group_schema, &self.group_ordering)?; } @@ -1338,7 +1350,7 @@ impl GroupedHashAggregateStream { fn update_skip_aggregation_probe(&mut self, input_rows: usize) { if let Some(probe) = self.skip_aggregation_probe.as_mut() { // Skip aggregation probe is not supported if stream has any spills, - // currently spilling is not supported for Partial aggregation + // currently spilling is not supported in Partial aggregation assert!(self.spill_state.spills.is_empty()); probe.update_state(input_rows, self.group_values.len()); }; @@ -1383,6 +1395,7 @@ impl GroupedHashAggregateStream { "group_values expected to have single element" ); let mut output = group_values.swap_remove(0); + cast_group_values_to_schema(&mut output, &self.schema)?; let iter = self .accumulators @@ -1406,13 +1419,66 @@ mod tests { use super::*; use crate::InputOrderMode; use crate::test::TestMemoryExec; - use arrow::array::{Int32Array, Int64Array}; - use arrow::datatypes::{DataType, Field, Schema}; + use arrow::array::{ + DictionaryArray, Int32Array, Int64Array, StringDictionaryBuilder, + }; + use arrow::datatypes::{DataType, Field, Schema, UInt8Type, UInt16Type}; use datafusion_execution::runtime_env::RuntimeEnvBuilder; use datafusion_functions_aggregate::count::count_udaf; use datafusion_physical_expr::aggregate::AggregateExprBuilder; use datafusion_physical_expr::expressions::col; + #[tokio::test] + async fn dictionary_group_key_promotes_runtime_schema() -> Result<()> { + let schema = Arc::new(Schema::new(vec![Field::new( + "group_col", + DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)), + false, + )])); + let batches = (0u32..257) + .map(|value| { + let mut builder = StringDictionaryBuilder::::new(); + builder.append_value(format!("group_{value}")); + RecordBatch::try_new( + Arc::clone(&schema), + vec![Arc::new(builder.finish())], + ) + }) + .collect::>>()?; + let exec = TestMemoryExec::try_new(&[batches], Arc::clone(&schema), None)?; + let exec = Arc::new(TestMemoryExec::update_cache(&Arc::new(exec))); + let aggregate_exec = AggregateExec::try_new( + AggregateMode::Single, + PhysicalGroupBy::new_single(vec![( + col("group_col", &schema)?, + "group_col".to_string(), + )]), + vec![], + vec![], + exec, + Arc::clone(&schema), + )?; + + let mut stream = GroupedHashAggregateStream::new( + &aggregate_exec, + &Arc::new(TaskContext::default()), + 0, + )?; + let batch = stream.next().await.unwrap()?; + assert_eq!(batch.num_rows(), 257); + assert_eq!( + batch.schema().field(0).data_type(), + &DataType::Dictionary(Box::new(DataType::UInt16), Box::new(DataType::Utf8)) + ); + assert!(batch + .column(0) + .as_any() + .downcast_ref::>() + .is_some()); + assert!(stream.next().await.is_none()); + Ok(()) + } + // Migrated to PartialHashAggregateStream coverage in hash_stream.rs; // kept here for the legacy GroupedHashAggregateStream implementation. #[tokio::test] From 80f89614633ccf3ec0eb2f24f45dad42ff0ec723 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:22:12 -0400 Subject: [PATCH 08/41] fix: distinguish dynamic output from stable aggregate transport --- .../src/aggregates/group_values/mod.rs | 160 +++++++++++------- 1 file changed, 102 insertions(+), 58 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/group_values/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/mod.rs index 33985d7ca142a..3da5fc9410f69 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/mod.rs @@ -24,12 +24,15 @@ use arrow::array::types::{ }; use arrow::array::{ArrayRef, downcast_primitive}; use arrow::compute::cast; -use arrow::datatypes::{DataType, Field, FieldRef, Schema, SchemaRef, TimeUnit}; -use datafusion_common::Result; +use arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit}; +use datafusion_common::{Result, internal_err}; use std::sync::Arc; use datafusion_expr::EmitTo; +const ORIGINAL_DICTIONARY_KEY_TYPE: &str = + "datafusion:group_value_original_dictionary_key_type"; + /// Returns `schema` with its leading group fields updated to the data types /// actually emitted by [`GroupValues`]. Dictionary key types may grow at /// runtime, while all field names, nullability, and metadata remain unchanged. @@ -67,8 +70,8 @@ pub(crate) fn schema_with_group_values( } /// Casts the leading group-value arrays to the corresponding schema fields. -/// This is used by spill files, which require one stable IPC schema even though -/// normal aggregate output may promote dictionary keys dynamically. +/// Stable partial/spill schemas use this before crossing fixed-schema transport +/// boundaries such as repartition coalescing and Arrow IPC. pub(crate) fn cast_group_values_to_schema( group_values: &mut [ArrayRef], schema: &SchemaRef, @@ -81,10 +84,23 @@ pub(crate) fn cast_group_values_to_schema( Ok(()) } -/// Returns a stable spill schema whose dictionary group keys use their widest -/// key type. The widening is internal to spill IPC and does not change planned -/// or non-spill aggregate output schemas. -pub(crate) fn group_value_spill_schema( +/// Returns true when the leading group fields use the stable transport encoding. +pub(crate) fn has_group_value_transport_fields( + schema: &SchemaRef, + num_group_fields: usize, +) -> bool { + schema + .fields() + .iter() + .take(num_group_fields) + .any(|field| field.metadata().contains_key(ORIGINAL_DICTIONARY_KEY_TYPE)) +} + +/// Returns a stable internal transport schema for partial aggregate and spill +/// batches. Dictionary group keys use the widest key with the same signedness, +/// while metadata records the original key type so final aggregate output can +/// start from the planned minimum width and promote only when necessary. +pub(crate) fn group_value_transport_schema( schema: &SchemaRef, num_group_fields: usize, ) -> SchemaRef { @@ -94,7 +110,7 @@ pub(crate) fn group_value_spill_schema( .enumerate() .map(|(index, field)| { if index < num_group_fields { - group_value_spill_field(field.as_ref()) + group_value_transport_field(field.as_ref()) } else { Arc::clone(field) } @@ -107,60 +123,88 @@ pub(crate) fn group_value_spill_schema( )) } -fn group_value_spill_field(field: &Field) -> FieldRef { +/// Restores the planned minimum dictionary key type from an internal transport +/// field. The transport marker is removed so it does not leak into final output. +pub(crate) fn group_value_output_field(field: Field) -> Result { + let Some(original_key) = field + .metadata() + .get(ORIGINAL_DICTIONARY_KEY_TYPE) + .cloned() + else { + return Ok(field); + }; + + let DataType::Dictionary(_, value_type) = field.data_type() else { + return internal_err!( + "Group value transport marker found on non-dictionary field {}", + field.name() + ); + }; + let value_type = value_type.as_ref().clone(); + let Some(key_type) = dictionary_key_type_from_name(&original_key) else { + return internal_err!( + "Invalid group value dictionary key type marker: {original_key}" + ); + }; + + let mut metadata = field.metadata().clone(); + metadata.remove(ORIGINAL_DICTIONARY_KEY_TYPE); + Ok(field + .with_data_type(DataType::Dictionary( + Box::new(key_type), + Box::new(value_type), + )) + .with_metadata(metadata)) +} + +fn group_value_transport_field(field: &Field) -> Arc { + let DataType::Dictionary(key_type, value_type) = field.data_type() else { + return Arc::new(field.clone()); + }; + let Some((original_key, transport_key)) = dictionary_transport_key(key_type) else { + return Arc::new(field.clone()); + }; + + let mut metadata = field.metadata().clone(); + metadata + .entry(ORIGINAL_DICTIONARY_KEY_TYPE.to_string()) + .or_insert_with(|| original_key.to_string()); + Arc::new( field .clone() - .with_data_type(group_value_spill_data_type(field.data_type())), + .with_data_type(DataType::Dictionary( + Box::new(transport_key), + Box::new(value_type.as_ref().clone()), + )) + .with_metadata(metadata), ) } -fn group_value_spill_data_type(data_type: &DataType) -> DataType { - match data_type { - DataType::Dictionary(key_type, value_type) => { - let key_type = match key_type.as_ref() { - DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => { - DataType::Int64 - } - DataType::UInt8 - | DataType::UInt16 - | DataType::UInt32 - | DataType::UInt64 => DataType::UInt64, - _ => key_type.as_ref().clone(), - }; - DataType::Dictionary( - Box::new(key_type), - Box::new(group_value_spill_data_type(value_type)), - ) - } - DataType::Struct(fields) => DataType::Struct( - fields - .iter() - .map(|field| group_value_spill_field(field.as_ref())) - .collect::>() - .into(), - ), - DataType::List(field) => DataType::List(group_value_spill_field(field.as_ref())), - DataType::LargeList(field) => { - DataType::LargeList(group_value_spill_field(field.as_ref())) - } - DataType::ListView(field) => { - DataType::ListView(group_value_spill_field(field.as_ref())) - } - DataType::LargeListView(field) => { - DataType::LargeListView(group_value_spill_field(field.as_ref())) - } - DataType::FixedSizeList(field, size) => { - DataType::FixedSizeList(group_value_spill_field(field.as_ref()), *size) - } - DataType::Map(field, sorted) => { - DataType::Map(group_value_spill_field(field.as_ref()), *sorted) - } - DataType::RunEndEncoded(run_ends, values) => DataType::RunEndEncoded( - Arc::clone(run_ends), - group_value_spill_field(values.as_ref()), - ), - _ => data_type.clone(), +fn dictionary_transport_key(key_type: &DataType) -> Option<(&'static str, DataType)> { + match key_type { + DataType::Int8 => Some(("Int8", DataType::Int64)), + DataType::Int16 => Some(("Int16", DataType::Int64)), + DataType::Int32 => Some(("Int32", DataType::Int64)), + DataType::UInt8 => Some(("UInt8", DataType::UInt64)), + DataType::UInt16 => Some(("UInt16", DataType::UInt64)), + DataType::UInt32 => Some(("UInt32", DataType::UInt64)), + DataType::Int64 | DataType::UInt64 => None, + _ => None, + } +} + +fn dictionary_key_type_from_name(name: &str) -> Option { + match name { + "Int8" => Some(DataType::Int8), + "Int16" => Some(DataType::Int16), + "Int32" => Some(DataType::Int32), + "Int64" => Some(DataType::Int64), + "UInt8" => Some(DataType::UInt8), + "UInt16" => Some(DataType::UInt16), + "UInt32" => Some(DataType::UInt32), + "UInt64" => Some(DataType::UInt64), + _ => None, } } @@ -265,7 +309,7 @@ pub trait GroupValues: Send { /// - Otherwise, the general implementation `GroupValuesRows` will be chosen. /// /// `GroupColumn`: crate::aggregates::group_values::multi_group_by::GroupColumn -/// `GroupValuesColumn`: crate::aggregates::group_values::multi_group_by::GroupValuesColumn +/// `GroupValuesColumn`: crate::aggregates::group_values::GroupValuesColumn /// `GroupValuesRows`: crate::aggregates::group_values::GroupValuesRows pub fn new_group_values( schema: SchemaRef, From 2d8d9e8257a6bbc920597034e9689f37f3040695 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:24:39 -0400 Subject: [PATCH 09/41] fix: keep dynamic group schema helpers self-contained --- .../src/aggregates/group_values/mod.rs | 158 +++++++----------- 1 file changed, 57 insertions(+), 101 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/group_values/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/mod.rs index 3da5fc9410f69..315952f43da22 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/mod.rs @@ -24,15 +24,12 @@ use arrow::array::types::{ }; use arrow::array::{ArrayRef, downcast_primitive}; use arrow::compute::cast; -use arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit}; -use datafusion_common::{Result, internal_err}; +use arrow::datatypes::{DataType, Field, FieldRef, Schema, SchemaRef, TimeUnit}; +use datafusion_common::Result; use std::sync::Arc; use datafusion_expr::EmitTo; -const ORIGINAL_DICTIONARY_KEY_TYPE: &str = - "datafusion:group_value_original_dictionary_key_type"; - /// Returns `schema` with its leading group fields updated to the data types /// actually emitted by [`GroupValues`]. Dictionary key types may grow at /// runtime, while all field names, nullability, and metadata remain unchanged. @@ -70,8 +67,8 @@ pub(crate) fn schema_with_group_values( } /// Casts the leading group-value arrays to the corresponding schema fields. -/// Stable partial/spill schemas use this before crossing fixed-schema transport -/// boundaries such as repartition coalescing and Arrow IPC. +/// This is used by spill files, which require one stable IPC schema even though +/// normal aggregate output may promote dictionary keys dynamically. pub(crate) fn cast_group_values_to_schema( group_values: &mut [ArrayRef], schema: &SchemaRef, @@ -84,23 +81,10 @@ pub(crate) fn cast_group_values_to_schema( Ok(()) } -/// Returns true when the leading group fields use the stable transport encoding. -pub(crate) fn has_group_value_transport_fields( - schema: &SchemaRef, - num_group_fields: usize, -) -> bool { - schema - .fields() - .iter() - .take(num_group_fields) - .any(|field| field.metadata().contains_key(ORIGINAL_DICTIONARY_KEY_TYPE)) -} - -/// Returns a stable internal transport schema for partial aggregate and spill -/// batches. Dictionary group keys use the widest key with the same signedness, -/// while metadata records the original key type so final aggregate output can -/// start from the planned minimum width and promote only when necessary. -pub(crate) fn group_value_transport_schema( +/// Returns a stable spill schema whose dictionary group keys use their widest +/// key type. The widening is internal to spill IPC and does not change planned +/// or non-spill aggregate output schemas. +pub(crate) fn group_value_spill_schema( schema: &SchemaRef, num_group_fields: usize, ) -> SchemaRef { @@ -110,7 +94,7 @@ pub(crate) fn group_value_transport_schema( .enumerate() .map(|(index, field)| { if index < num_group_fields { - group_value_transport_field(field.as_ref()) + group_value_spill_field(field.as_ref()) } else { Arc::clone(field) } @@ -123,88 +107,60 @@ pub(crate) fn group_value_transport_schema( )) } -/// Restores the planned minimum dictionary key type from an internal transport -/// field. The transport marker is removed so it does not leak into final output. -pub(crate) fn group_value_output_field(field: Field) -> Result { - let Some(original_key) = field - .metadata() - .get(ORIGINAL_DICTIONARY_KEY_TYPE) - .cloned() - else { - return Ok(field); - }; - - let DataType::Dictionary(_, value_type) = field.data_type() else { - return internal_err!( - "Group value transport marker found on non-dictionary field {}", - field.name() - ); - }; - let value_type = value_type.as_ref().clone(); - let Some(key_type) = dictionary_key_type_from_name(&original_key) else { - return internal_err!( - "Invalid group value dictionary key type marker: {original_key}" - ); - }; - - let mut metadata = field.metadata().clone(); - metadata.remove(ORIGINAL_DICTIONARY_KEY_TYPE); - Ok(field - .with_data_type(DataType::Dictionary( - Box::new(key_type), - Box::new(value_type), - )) - .with_metadata(metadata)) -} - -fn group_value_transport_field(field: &Field) -> Arc { - let DataType::Dictionary(key_type, value_type) = field.data_type() else { - return Arc::new(field.clone()); - }; - let Some((original_key, transport_key)) = dictionary_transport_key(key_type) else { - return Arc::new(field.clone()); - }; - - let mut metadata = field.metadata().clone(); - metadata - .entry(ORIGINAL_DICTIONARY_KEY_TYPE.to_string()) - .or_insert_with(|| original_key.to_string()); - +fn group_value_spill_field(field: &Field) -> FieldRef { Arc::new( field .clone() - .with_data_type(DataType::Dictionary( - Box::new(transport_key), - Box::new(value_type.as_ref().clone()), - )) - .with_metadata(metadata), + .with_data_type(group_value_spill_data_type(field.data_type())), ) } -fn dictionary_transport_key(key_type: &DataType) -> Option<(&'static str, DataType)> { - match key_type { - DataType::Int8 => Some(("Int8", DataType::Int64)), - DataType::Int16 => Some(("Int16", DataType::Int64)), - DataType::Int32 => Some(("Int32", DataType::Int64)), - DataType::UInt8 => Some(("UInt8", DataType::UInt64)), - DataType::UInt16 => Some(("UInt16", DataType::UInt64)), - DataType::UInt32 => Some(("UInt32", DataType::UInt64)), - DataType::Int64 | DataType::UInt64 => None, - _ => None, - } -} - -fn dictionary_key_type_from_name(name: &str) -> Option { - match name { - "Int8" => Some(DataType::Int8), - "Int16" => Some(DataType::Int16), - "Int32" => Some(DataType::Int32), - "Int64" => Some(DataType::Int64), - "UInt8" => Some(DataType::UInt8), - "UInt16" => Some(DataType::UInt16), - "UInt32" => Some(DataType::UInt32), - "UInt64" => Some(DataType::UInt64), - _ => None, +fn group_value_spill_data_type(data_type: &DataType) -> DataType { + match data_type { + DataType::Dictionary(key_type, value_type) => { + let key_type = match key_type.as_ref() { + DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => { + DataType::Int64 + } + DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 => DataType::UInt64, + _ => key_type.as_ref().clone(), + }; + DataType::Dictionary( + Box::new(key_type), + Box::new(group_value_spill_data_type(value_type)), + ) + } + DataType::Struct(fields) => DataType::Struct( + fields + .iter() + .map(|field| group_value_spill_field(field.as_ref())) + .collect::>() + .into(), + ), + DataType::List(field) => DataType::List(group_value_spill_field(field.as_ref())), + DataType::LargeList(field) => { + DataType::LargeList(group_value_spill_field(field.as_ref())) + } + DataType::ListView(field) => { + DataType::ListView(group_value_spill_field(field.as_ref())) + } + DataType::LargeListView(field) => { + DataType::LargeListView(group_value_spill_field(field.as_ref())) + } + DataType::FixedSizeList(field, size) => { + DataType::FixedSizeList(group_value_spill_field(field.as_ref()), *size) + } + DataType::Map(field, sorted) => { + DataType::Map(group_value_spill_field(field.as_ref()), *sorted) + } + DataType::RunEndEncoded(run_ends, values) => DataType::RunEndEncoded( + Arc::clone(run_ends), + group_value_spill_field(values.as_ref()), + ), + _ => data_type.clone(), } } From 1dff0a2bb1ee2dbc135ad39e68e5dfc3570e93eb Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:28:11 -0400 Subject: [PATCH 10/41] fix: bound partial dictionary group batches by key capacity --- .../src/aggregates/group_values/mod.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/datafusion/physical-plan/src/aggregates/group_values/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/mod.rs index 315952f43da22..23d90b9c28de1 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/mod.rs @@ -66,6 +66,44 @@ pub(crate) fn schema_with_group_values( )) } +/// Returns the largest partial aggregate batch that can be emitted without +/// changing any top-level dictionary group key type in `schema`. +/// +/// Partial aggregate batches cross fixed-schema transport boundaries such as +/// repartition coalescing. Splitting them at the dictionary key capacity keeps +/// that transport schema stable; the final aggregate can still combine all +/// groups and promote its output key type dynamically. +pub(crate) fn group_value_emit_batch_size( + schema: &SchemaRef, + num_group_fields: usize, + batch_size: usize, +) -> usize { + schema + .fields() + .iter() + .take(num_group_fields) + .filter_map(|field| match field.data_type() { + DataType::Dictionary(key_type, _) => dictionary_key_capacity(key_type), + _ => None, + }) + .fold(batch_size, usize::min) + .max(1) +} + +fn dictionary_key_capacity(key_type: &DataType) -> Option { + let capacity = match key_type { + DataType::Int8 => 1u64 << 7, + DataType::Int16 => 1u64 << 15, + DataType::Int32 => 1u64 << 31, + DataType::UInt8 => 1u64 << 8, + DataType::UInt16 => 1u64 << 16, + DataType::UInt32 => 1u64 << 32, + DataType::Int64 | DataType::UInt64 => return Some(usize::MAX), + _ => return None, + }; + Some(usize::try_from(capacity).unwrap_or(usize::MAX)) +} + /// Casts the leading group-value arrays to the corresponding schema fields. /// This is used by spill files, which require one stable IPC schema even though /// normal aggregate output may promote dictionary keys dynamically. From fe1131a1bd1fe4976f631468891cb662aa63d514 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:29:19 -0400 Subject: [PATCH 11/41] fix: emit schema-stable partial dictionary batches --- .../aggregates/aggregate_hash_table/common.rs | 76 +++++++++++++++++-- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs index 7c7c2cba836c3..f500c72b22e07 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs @@ -28,7 +28,8 @@ use datafusion_physical_expr::aggregate::AggregateFunctionExpr; use crate::PhysicalExpr; use crate::aggregates::group_values::{ - GroupByMetrics, GroupValues, new_group_values, schema_with_group_values, + GroupByMetrics, GroupValues, group_value_emit_batch_size, new_group_values, + schema_with_group_values, }; use crate::aggregates::grouped_hash_stream::create_group_accumulator; use crate::aggregates::order::GroupOrdering; @@ -207,12 +208,74 @@ impl AggregateHashTable { Ok(()) } + /// Emits one partial-state batch without changing the planned schema. + /// + /// Dictionary group columns are capped at the number of values representable + /// by their planned key type. Remaining groups stay in the table and are + /// emitted on later polls. This keeps repartition/coalescing schemas stable + /// while allowing a downstream final aggregate to combine all groups and + /// promote its final output key type when required. + pub(super) fn next_partial_output_batch_inner( + &mut self, + materialize_accumulator_fn: MaterializeAccumulatorFn, + ) -> Result> { + let output_schema = Arc::clone(&self.output_schema); + let batch_size = self.batch_size; + + let mut state = + match std::mem::replace(&mut self.state, AggregateHashTableState::Done) { + AggregateHashTableState::Outputting(state) => state, + AggregateHashTableState::Done => return Ok(None), + AggregateHashTableState::Building(_) => { + return internal_err!( + "next_output_batch must be called in the outputting state" + ); + } + AggregateHashTableState::OutputtingMaterialized(_) => { + return internal_err!( + "partial aggregate output must not be materialized" + ); + } + }; + + if state.group_values.is_empty() { + return Ok(None); + } + + let max_groups = group_value_emit_batch_size( + &output_schema, + state.group_by.num_group_exprs(), + batch_size, + ); + let num_groups = max_groups.min(state.group_values.len()); + let emit_to = if num_groups == state.group_values.len() { + EmitTo::All + } else { + EmitTo::First(num_groups) + }; + + let timer = self.group_by_metrics.emitting_time.timer(); + let mut columns = state.group_values.emit(emit_to)?; + for acc in state.accumulators.iter_mut() { + columns.extend(materialize_accumulator_fn(acc, emit_to)?); + } + drop(timer); + + let batch = RecordBatch::try_new(output_schema, columns)?; + debug_assert!(batch.num_rows() > 0); + if state.group_values.is_empty() { + self.state = AggregateHashTableState::Done; + } else { + self.state = AggregateHashTableState::Outputting(state); + } + Ok(Some(batch)) + } + /// Materializes the full output once, then returns it downstream incrementally /// by slicing it into `batch_size` chunks. /// - /// Each aggregation mode chooses a different `materialize_accumulator_fn` - /// according to its semantics. For example, partial aggregation emits - /// partial states to feed the final stage, so it uses [`GroupsAccumulator::state`]. + /// Final aggregate evaluation consumes accumulator state, so it is + /// materialized once and then sliced on later polls. /// /// This is a temporary solution until blocked state management is implemented: /// Issue: @@ -430,9 +493,8 @@ pub(super) enum AggregateHashTableState { /// Fully evaluated aggregate output and the next row offset to emit. /// -/// Final aggregate evaluation consumes accumulator state, and partial terminal -/// output should not repeatedly renumber group values with `EmitTo::First`. -/// Materialize once and then slice to honor `batch_size` across output polls. +/// Final aggregate evaluation consumes accumulator state, so it is materialized +/// once and then sliced to honor `batch_size` across output polls. pub(super) struct MaterializedAggregateOutput { batch: RecordBatch, offset: usize, From b541d9d548a83561ff809230bf91bba30266e5ac Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:29:43 -0400 Subject: [PATCH 12/41] fix: keep partial dictionary output schema stable --- .../src/aggregates/aggregate_hash_table/partial_table.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_table.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_table.rs index ffac42feaa3b3..81d29ba4ff240 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_table.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_table.rs @@ -58,14 +58,15 @@ impl AggregateHashTable { /// Emits the next batch of aggregated group keys and aggregate states. /// - /// The output batch size is determined by `self.batch_size`. + /// The output batch size is bounded by both `self.batch_size` and any + /// dictionary group key capacity required to preserve the planned schema. /// /// Returns `Some(batch)` for each emitted batch, `None` when output is /// exhausted, and an internal error if polled in the `Building` state. pub(in crate::aggregates) fn next_output_batch( &mut self, ) -> Result> { - self.next_output_batch_inner(HashAggregateAccumulator::state) + self.next_partial_output_batch_inner(HashAggregateAccumulator::state) } pub(in crate::aggregates) fn can_skip_aggregation(&self) -> bool { From 063461e98cdf21d25b14e1dd3264843dc66234aa Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:29:57 -0400 Subject: [PATCH 13/41] fix: keep partial-reduce dictionary output schema stable --- .../aggregates/aggregate_hash_table/partial_reduce_table.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_reduce_table.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_reduce_table.rs index d8e92c5928b8a..d27df2dea933e 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_reduce_table.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/partial_reduce_table.rs @@ -42,14 +42,15 @@ impl AggregateHashTable { /// Emits the next batch of aggregated group keys and aggregate states. /// - /// The output batch size is determined by `self.batch_size`. + /// The output batch size is bounded by both `self.batch_size` and any + /// dictionary group key capacity required to preserve the planned schema. /// /// Returns `Some(batch)` for each emitted batch, `None` when output is /// exhausted, and an internal error if polled in the `Building` state. pub(in crate::aggregates) fn next_output_batch( &mut self, ) -> Result> { - self.next_output_batch_inner(HashAggregateAccumulator::state) + self.next_partial_output_batch_inner(HashAggregateAccumulator::state) } /// Partial-reduce aggregation consumes partial aggregate states and merges From 023a0fe6979fdda6cea34b4afeec8b80b77c0cb6 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:30:49 -0400 Subject: [PATCH 14/41] fix: keep ordered partial dictionary output schema stable --- .../aggregate_hash_table/common_ordered.rs | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs index ec6d0c066d701..855568fa39aa0 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs @@ -31,7 +31,8 @@ use datafusion_expr::EmitTo; use crate::InputOrderMode; use crate::PhysicalExpr; use crate::aggregates::group_values::{ - GroupByMetrics, GroupValues, new_group_values, schema_with_group_values, + GroupByMetrics, GroupValues, group_value_emit_batch_size, new_group_values, + schema_with_group_values, }; use crate::aggregates::grouped_hash_stream::create_group_accumulator; use crate::aggregates::order::GroupOrdering; @@ -236,7 +237,7 @@ impl OrderedAggregateTable { + self.buffer.group_indices.allocated_size() } - /// Returns the [`EmitTo`], clamped to the specified batch size + /// Returns the [`EmitTo`], clamped to the specified maximum batch size. /// /// Returns `(emit_to, should_remove_groups)`, where `emit_to` is the number /// of groups to emit from `GroupValues` / accumulators, and @@ -246,13 +247,15 @@ impl OrderedAggregateTable { &self, group_count: usize, emit_to: EmitTo, + max_batch_size: usize, ) -> (EmitTo, bool) { match emit_to { - EmitTo::First(n) => (EmitTo::First(n.min(self.batch_size)), true), - EmitTo::All if group_count <= self.batch_size => (EmitTo::All, false), - EmitTo::All => (EmitTo::First(self.batch_size), false), + EmitTo::First(n) => (EmitTo::First(n.min(max_batch_size)), true), + EmitTo::All if group_count <= max_batch_size => (EmitTo::All, false), + EmitTo::All => (EmitTo::First(max_batch_size), false), } } + /// Aggregates one evaluated input batch. /// /// This common utility is used by ordered partial and final aggregation. @@ -314,8 +317,8 @@ impl OrderedAggregateTable { /// /// # Argument: `is_final` /// - /// - `true`: output final aggregate values. - /// - `false`: output partial accumulator states. + /// - `true`: output final aggregate values and permit dictionary key promotion. + /// - `false`: output partial accumulator states with the planned schema. pub(super) fn next_output_batch_for_mode( &mut self, is_final: bool, @@ -327,8 +330,20 @@ impl OrderedAggregateTable { let Some(emit_to) = self.buffer.group_ordering.emit_to() else { return Ok(None); }; - let (emit_to, should_remove_groups) = - self.clamp_emit_to(self.buffer.group_values.len(), emit_to); + let max_batch_size = if is_final { + self.batch_size + } else { + group_value_emit_batch_size( + &self.output_schema, + self.buffer.group_by.num_group_exprs(), + self.batch_size, + ) + }; + let (emit_to, should_remove_groups) = self.clamp_emit_to( + self.buffer.group_values.len(), + emit_to, + max_batch_size, + ); let timer = self.group_by_metrics.emitting_time.timer(); let mut output = self.buffer.group_values.emit(emit_to)?; @@ -352,10 +367,14 @@ impl OrderedAggregateTable { } drop(timer); - let output_schema = schema_with_group_values( - &self.output_schema, - &output[..num_group_columns], - ); + let output_schema = if is_final { + schema_with_group_values( + &self.output_schema, + &output[..num_group_columns], + ) + } else { + Arc::clone(&self.output_schema) + }; let batch = RecordBatch::try_new(output_schema, output)?; debug_assert!(batch.num_rows() > 0); From 44829fb836468de56d724b3c29ae1f661731e00f Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:32:04 -0400 Subject: [PATCH 15/41] test: add aggregate hash table dictionary regressions --- .../physical-plan/src/aggregates/aggregate_hash_table/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/mod.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/mod.rs index 0d2495a1b556c..8e969c55a6a5b 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/mod.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/mod.rs @@ -28,3 +28,6 @@ pub(super) use common::{ PartialSkipMarker, }; pub(super) use common_ordered::OrderedAggregateTable; + +#[cfg(test)] +mod tests; From 60afd283226f48aa594a705eaee990084467cdeb Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:32:30 -0400 Subject: [PATCH 16/41] test: cover dictionary promotion across partial and final aggregation --- .../aggregates/aggregate_hash_table/tests.rs | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs new file mode 100644 index 0000000000000..9c455e1994f99 --- /dev/null +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs @@ -0,0 +1,149 @@ +// 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 std::sync::Arc; + +use arrow::array::{ + ArrayRef, DictionaryArray, StringArray, StringDictionaryBuilder, +}; +use arrow::datatypes::{DataType, Field, Schema, UInt8Type, UInt16Type}; +use arrow::record_batch::RecordBatch; +use datafusion_common::Result; +use datafusion_physical_expr::expressions::col; + +use crate::aggregates::{AggregateExec, AggregateMode, PhysicalGroupBy}; +use crate::test::TestMemoryExec; + +use super::{AggregateHashTable, FinalMarker, PartialMarker}; + +#[test] +fn dictionary_groups_keep_partial_schema_and_promote_final_output() -> Result<()> { + let input_schema = Arc::new(Schema::new(vec![Field::new( + "group_col", + DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)), + false, + )])); + let input_batches = (0u32..257) + .map(|value| { + let mut builder = StringDictionaryBuilder::::new(); + builder.append_value(format!("group_{value}")); + let array: ArrayRef = Arc::new(builder.finish()); + RecordBatch::try_new(Arc::clone(&input_schema), vec![array]) + }) + .collect::>>()?; + + let input = TestMemoryExec::try_new( + &[input_batches.clone()], + Arc::clone(&input_schema), + None, + )?; + let input = Arc::new(TestMemoryExec::update_cache(&Arc::new(input))); + let group_by = PhysicalGroupBy::new_single(vec![( + col("group_col", &input_schema)?, + "group_col".to_string(), + )]); + let partial_exec = AggregateExec::try_new( + AggregateMode::Partial, + group_by.clone(), + vec![], + vec![], + input, + Arc::clone(&input_schema), + )?; + let partial_schema = Arc::clone(&partial_exec.schema); + let mut partial_table = AggregateHashTable::::new( + &partial_exec, + 0, + Arc::clone(&partial_schema), + 1024, + )?; + for batch in &input_batches { + partial_table.aggregate_batch(batch)?; + } + partial_table.start_output()?; + + let mut partial_batches = vec![]; + while let Some(batch) = partial_table.next_output_batch()? { + assert_eq!(batch.schema(), partial_schema); + assert_eq!( + batch.schema().field(0).data_type(), + &DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)) + ); + assert!(batch + .column(0) + .as_any() + .downcast_ref::>() + .is_some()); + partial_batches.push(batch); + } + assert_eq!( + partial_batches + .iter() + .map(RecordBatch::num_rows) + .collect::>(), + vec![256, 1] + ); + + let partial_input = TestMemoryExec::try_new( + &[partial_batches.clone()], + Arc::clone(&partial_schema), + None, + )?; + let partial_input = Arc::new(TestMemoryExec::update_cache(&Arc::new(partial_input))); + let final_exec = AggregateExec::try_new( + AggregateMode::Final, + group_by.as_final(), + vec![], + vec![], + partial_input, + Arc::clone(&input_schema), + )?; + let mut final_table = AggregateHashTable::::new( + &final_exec, + 0, + Arc::clone(&final_exec.schema), + 1024, + )?; + for batch in &partial_batches { + final_table.aggregate_batch(batch)?; + } + final_table.start_output()?; + + let batch = final_table.next_output_batch()?.unwrap(); + assert_eq!(batch.num_rows(), 257); + assert_eq!( + batch.schema().field(0).data_type(), + &DataType::Dictionary(Box::new(DataType::UInt16), Box::new(DataType::Utf8)) + ); + let dictionary = batch + .column(0) + .as_any() + .downcast_ref::>() + .unwrap(); + let values = dictionary + .values() + .as_any() + .downcast_ref::() + .unwrap(); + for row in 0..257 { + let key = dictionary.keys().value(row) as usize; + assert_eq!(values.value(key), format!("group_{row}")); + } + assert!(final_table.next_output_batch()?.is_none()); + + Ok(()) +} From a9fc6be56862b6a77325195518fa6a3f91f351cd Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:33:28 -0400 Subject: [PATCH 17/41] test: use DataFusion result for dictionary batches --- .../src/aggregates/aggregate_hash_table/tests.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs index 9c455e1994f99..98eeb6cdd7382 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs @@ -38,11 +38,14 @@ fn dictionary_groups_keep_partial_schema_and_promote_final_output() -> Result<() false, )])); let input_batches = (0u32..257) - .map(|value| { + .map(|value| -> Result { let mut builder = StringDictionaryBuilder::::new(); builder.append_value(format!("group_{value}")); let array: ArrayRef = Arc::new(builder.finish()); - RecordBatch::try_new(Arc::clone(&input_schema), vec![array]) + Ok(RecordBatch::try_new( + Arc::clone(&input_schema), + vec![array], + )?) }) .collect::>>()?; From 917aed8380fc0c459775fd277fcffbc5bb378f3d Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:35:03 -0400 Subject: [PATCH 18/41] test: cover dictionary key promotion through repartitioned aggregation --- .../dictionary_group_by_key_promotion.slt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt diff --git a/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt b/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt new file mode 100644 index 0000000000000..a5d7c62cf3d85 --- /dev/null +++ b/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt @@ -0,0 +1,50 @@ +# 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. + +# Each input batch can be encoded with UInt8, but the complete GROUP BY result +# has 257 distinct values and therefore requires UInt16 dictionary keys. +statement ok +SET datafusion.execution.batch_size = 128; + +statement ok +SET datafusion.execution.target_partitions = 4; + +query I +SELECT COUNT(*) +FROM ( + SELECT k + FROM ( + SELECT arrow_cast(CAST(value AS VARCHAR), 'Dictionary(UInt8, Utf8)') AS k + FROM generate_series(0, 256) + ) + GROUP BY k +); +---- +257 + +query T rowsort +SELECT DISTINCT arrow_typeof(k) +FROM ( + SELECT k + FROM ( + SELECT arrow_cast(CAST(value AS VARCHAR), 'Dictionary(UInt8, Utf8)') AS k + FROM generate_series(0, 256) + ) + GROUP BY k +); +---- +Dictionary(UInt16, Utf8) From 192693f6e530c85144e833d05ab77ff949d29e15 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:40:05 -0400 Subject: [PATCH 19/41] fix: keep legacy partial dictionary output schema stable --- .../src/aggregates/grouped_hash_stream.rs | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs b/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs index 3df5c4aceb305..83ab3f570a542 100644 --- a/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs +++ b/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs @@ -26,7 +26,8 @@ use super::skip_partial::SkipAggregationProbe; use super::{AggregateExec, format_human_display}; use crate::aggregates::group_values::{ GroupByMetrics, GroupValues, cast_group_values_to_schema, - group_value_spill_schema, new_group_values, schema_with_group_values, + group_value_emit_batch_size, group_value_spill_schema, new_group_values, + schema_with_group_values, }; use crate::aggregates::order::GroupOrderingFull; use crate::aggregates::{ @@ -765,7 +766,7 @@ impl Stream for GroupedHashAggregateStream { if !self.group_values.is_empty() { return Poll::Ready(Some(internal_err!( "Switching from SkippingAggregation to Done with {} groups still in hash table. \ -This is a bug - all groups should have been emitted before skip aggregation started.", + This is a bug - all groups should have been emitted before skip aggregation started.", self.group_values.len() ))); } @@ -785,12 +786,18 @@ This is a bug - all groups should have been emitted before skip aggregation star (self.exec_state, output_batch) = if batch.num_rows() <= size { ( if self.input_done { - ExecutionState::Done + if self.group_values.is_empty() { + ExecutionState::Done + } else { + ExecutionState::ReadingInput + } } // In Partial aggregation, we also need to check - // if we should trigger partial skipping + // if we should trigger partial skipping after every + // accumulated group has been emitted. else if self.mode == AggregateMode::Partial && self.should_skip_aggregation() + && self.group_values.is_empty() { ExecutionState::SkippingAggregation } else { @@ -824,7 +831,7 @@ This is a bug - all groups should have been emitted before skip aggregation star if !self.group_values.is_empty() { return Poll::Ready(Some(internal_err!( "AggregateStream was in Done state with {} groups left in hash table. \ -This is a bug - all groups should have been emitted before entering Done state.", + This is a bug - all groups should have been emitted before entering Done state.", self.group_values.len() ))); } @@ -1032,6 +1039,25 @@ impl GroupedHashAggregateStream { return Ok(None); } + let emit_to = if !spilling + && self.mode.output_mode() == AggregateOutputMode::Partial + { + let max_groups = group_value_emit_batch_size( + &self.schema, + self.group_by.num_group_exprs(), + self.batch_size, + ); + match emit_to { + EmitTo::First(n) => EmitTo::First(n.min(max_groups)), + EmitTo::All if self.group_values.len() > max_groups => { + EmitTo::First(max_groups) + } + EmitTo::All => EmitTo::All, + } + } else { + emit_to + }; + let timer = self.group_by_metrics.emitting_time.timer(); let mut output = self.group_values.emit(emit_to)?; let num_group_columns = output.len(); @@ -1041,6 +1067,8 @@ impl GroupedHashAggregateStream { &self.spill_state.spill_schema, )?; Arc::clone(&self.spill_state.spill_schema) + } else if self.mode.output_mode() == AggregateOutputMode::Partial { + Arc::clone(&self.schema) } else { let schema = schema_with_group_values( &self.schema, @@ -1436,13 +1464,13 @@ mod tests { false, )])); let batches = (0u32..257) - .map(|value| { + .map(|value| -> Result { let mut builder = StringDictionaryBuilder::::new(); builder.append_value(format!("group_{value}")); - RecordBatch::try_new( + Ok(RecordBatch::try_new( Arc::clone(&schema), vec![Arc::new(builder.finish())], - ) + )?) }) .collect::>>()?; let exec = TestMemoryExec::try_new(&[batches], Arc::clone(&schema), None)?; From 47a4f3d5683d43fa25839da64532d2a6c5f0d5e1 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:42:24 -0400 Subject: [PATCH 20/41] test: make dictionary regression series column explicit --- .../test_files/dictionary_group_by_key_promotion.slt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt b/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt index a5d7c62cf3d85..4a36e71670a0c 100644 --- a/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt +++ b/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt @@ -29,7 +29,7 @@ FROM ( SELECT k FROM ( SELECT arrow_cast(CAST(value AS VARCHAR), 'Dictionary(UInt8, Utf8)') AS k - FROM generate_series(0, 256) + FROM generate_series(0, 256) AS series(value) ) GROUP BY k ); @@ -42,7 +42,7 @@ FROM ( SELECT k FROM ( SELECT arrow_cast(CAST(value AS VARCHAR), 'Dictionary(UInt8, Utf8)') AS k - FROM generate_series(0, 256) + FROM generate_series(0, 256) AS series(value) ) GROUP BY k ); From eb8f4eebdb740ea609a631748b857172ae508ab2 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:43:28 -0400 Subject: [PATCH 21/41] test: separate partial transport and final promotion coverage --- .../dictionary_group_by_key_promotion.slt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt b/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt index 4a36e71670a0c..04e080d2e7d3d 100644 --- a/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt +++ b/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt @@ -15,14 +15,33 @@ # specific language governing permissions and limitations # under the License. -# Each input batch can be encoded with UInt8, but the complete GROUP BY result -# has 257 distinct values and therefore requires UInt16 dictionary keys. +# Each input dictionary batch fits in UInt8. With four target partitions and a +# batch size of 128, one partial aggregate receives 257 distinct groups. Its +# output must be split into schema-stable UInt8 batches before repartitioning. statement ok SET datafusion.execution.batch_size = 128; statement ok SET datafusion.execution.target_partitions = 4; +query I +SELECT COUNT(*) +FROM ( + SELECT k + FROM ( + SELECT arrow_cast(CAST(value AS VARCHAR), 'Dictionary(UInt8, Utf8)') AS k + FROM generate_series(0, 1024) AS series(value) + ) + GROUP BY k +); +---- +1025 + +# A single final aggregate sees all 257 groups and must promote the emitted +# dictionary key type from UInt8 to UInt16. +statement ok +SET datafusion.execution.target_partitions = 1; + query I SELECT COUNT(*) FROM ( From f575ad3ccfe9d88c820e87538f83357b6c1ebc54 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:47:21 -0400 Subject: [PATCH 22/41] fix: limit dictionary promotion to top-level group columns --- .../src/aggregates/group_values/row.rs | 80 ++++++++----------- 1 file changed, 34 insertions(+), 46 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/group_values/row.rs b/datafusion/physical-plan/src/aggregates/group_values/row.rs index a149e46ca6bc4..2aecd4cd1d53a 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/row.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/row.rs @@ -21,7 +21,7 @@ use arrow::array::{ downcast_run_end_index, }; use arrow::compute::cast; -use arrow::datatypes::{DataType, FieldRef, Schema, SchemaRef}; +use arrow::datatypes::{DataType, Schema, SchemaRef}; use arrow::error::ArrowError; use arrow::row::{RowConverter, Rows, SortField}; use datafusion_common::Result; @@ -263,11 +263,11 @@ impl GroupValues for GroupValuesRows { } }; - // Re-encode dictionary group keys using the current key type. If the - // emitted cardinality no longer fits, grow to the next key width and + // Re-encode top-level dictionary group keys using the current key type. + // If an emitted array no longer fits, grow to the next key width and // retain that promoted type for all later emissions. for (field, array) in self.schema.fields().iter().zip(&mut output) { - *array = dictionary_encode_if_necessary(array, field.data_type())?; + *array = dictionary_encode_with_promotion(array, field.data_type())?; } self.schema = schema_with_group_values(&self.schema, &output); @@ -308,6 +308,29 @@ fn materialized_row_schema(schema: &SchemaRef) -> SchemaRef { )) } +fn dictionary_encode_with_promotion( + array: &ArrayRef, + expected: &DataType, +) -> Result { + if !matches!(expected, DataType::Dictionary(_, _)) { + return dictionary_encode_if_necessary(array, expected); + } + + let mut target = expected.clone(); + loop { + match cast(array.as_ref(), &target) { + Ok(array) => return Ok(array), + Err(ArrowError::DictionaryKeyOverflowError) => { + let Some(promoted) = promote_dictionary_type(&target) else { + return Err(ArrowError::DictionaryKeyOverflowError.into()); + }; + target = promoted; + } + Err(error) => return Err(error.into()), + } + } +} + fn dictionary_encode_if_necessary( array: &ArrayRef, expected: &DataType, @@ -322,49 +345,27 @@ fn dictionary_encode_if_necessary( dictionary_encode_if_necessary(column, expected_field.data_type()) }) .collect::>>()?; - let fields = expected_fields - .iter() - .zip(&arrays) - .map(|(field, array)| field_with_array_type(field, array)) - .collect::>() - .into(); Ok(Arc::new(StructArray::try_new( - fields, + expected_fields.clone(), arrays, struct_array.nulls().cloned(), )?)) } (DataType::List(expected_field), &DataType::List(_)) => { let list = array.as_any().downcast_ref::().unwrap(); - let values = dictionary_encode_if_necessary( - list.values(), - expected_field.data_type(), - )?; - let field = field_with_array_type(expected_field, &values); Ok(Arc::new(ListArray::try_new( - field, + Arc::::clone(expected_field), list.offsets().clone(), - values, + dictionary_encode_if_necessary( + list.values(), + expected_field.data_type(), + )?, list.nulls().cloned(), )?)) } - (DataType::Dictionary(_, _), _) => { - let mut target = expected.clone(); - loop { - match cast(array.as_ref(), &target) { - Ok(array) => return Ok(array), - Err(ArrowError::DictionaryKeyOverflowError) => { - let Some(promoted) = promote_dictionary_type(&target) else { - return Err(ArrowError::DictionaryKeyOverflowError.into()); - }; - target = promoted; - } - Err(error) => return Err(error.into()), - } - } - } + (DataType::Dictionary(_, _), _) => Ok(cast(array.as_ref(), expected)?), ( DataType::RunEndEncoded(run_ends_field, expected_values_field), &DataType::RunEndEncoded(_, _), @@ -396,19 +397,6 @@ fn dictionary_encode_if_necessary( } } -fn field_with_array_type(field: &FieldRef, array: &ArrayRef) -> FieldRef { - if field.data_type() == array.data_type() { - Arc::clone(field) - } else { - Arc::new( - field - .as_ref() - .clone() - .with_data_type(array.data_type().clone()), - ) - } -} - fn promote_dictionary_type(data_type: &DataType) -> Option { let DataType::Dictionary(key_type, value_type) = data_type else { return None; From 385383bfff22db3819b2eb2afe40d1be78125e7a Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:48:05 -0400 Subject: [PATCH 23/41] fix: keep dictionary schema helpers top-level and scoped --- .../src/aggregates/group_values/mod.rs | 55 +++---------------- 1 file changed, 9 insertions(+), 46 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/group_values/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/mod.rs index 23d90b9c28de1..97409dfb69a0b 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/mod.rs @@ -119,9 +119,9 @@ pub(crate) fn cast_group_values_to_schema( Ok(()) } -/// Returns a stable spill schema whose dictionary group keys use their widest -/// key type. The widening is internal to spill IPC and does not change planned -/// or non-spill aggregate output schemas. +/// Returns a stable spill schema whose top-level dictionary group keys use +/// their widest key type. This widening is internal to spill IPC and does not +/// change planned or non-spill aggregate output schemas. pub(crate) fn group_value_spill_schema( schema: &SchemaRef, num_group_fields: usize, @@ -146,15 +146,7 @@ pub(crate) fn group_value_spill_schema( } fn group_value_spill_field(field: &Field) -> FieldRef { - Arc::new( - field - .clone() - .with_data_type(group_value_spill_data_type(field.data_type())), - ) -} - -fn group_value_spill_data_type(data_type: &DataType) -> DataType { - match data_type { + let data_type = match field.data_type() { DataType::Dictionary(key_type, value_type) => { let key_type = match key_type.as_ref() { DataType::Int8 | DataType::Int16 | DataType::Int32 | DataType::Int64 => { @@ -166,40 +158,11 @@ fn group_value_spill_data_type(data_type: &DataType) -> DataType { | DataType::UInt64 => DataType::UInt64, _ => key_type.as_ref().clone(), }; - DataType::Dictionary( - Box::new(key_type), - Box::new(group_value_spill_data_type(value_type)), - ) - } - DataType::Struct(fields) => DataType::Struct( - fields - .iter() - .map(|field| group_value_spill_field(field.as_ref())) - .collect::>() - .into(), - ), - DataType::List(field) => DataType::List(group_value_spill_field(field.as_ref())), - DataType::LargeList(field) => { - DataType::LargeList(group_value_spill_field(field.as_ref())) - } - DataType::ListView(field) => { - DataType::ListView(group_value_spill_field(field.as_ref())) - } - DataType::LargeListView(field) => { - DataType::LargeListView(group_value_spill_field(field.as_ref())) + DataType::Dictionary(Box::new(key_type), Box::new(value_type.as_ref().clone())) } - DataType::FixedSizeList(field, size) => { - DataType::FixedSizeList(group_value_spill_field(field.as_ref()), *size) - } - DataType::Map(field, sorted) => { - DataType::Map(group_value_spill_field(field.as_ref()), *sorted) - } - DataType::RunEndEncoded(run_ends, values) => DataType::RunEndEncoded( - Arc::clone(run_ends), - group_value_spill_field(values.as_ref()), - ), - _ => data_type.clone(), - } + _ => field.data_type().clone(), + }; + Arc::new(field.clone().with_data_type(data_type)) } pub mod multi_group_by; @@ -303,7 +266,7 @@ pub trait GroupValues: Send { /// - Otherwise, the general implementation `GroupValuesRows` will be chosen. /// /// `GroupColumn`: crate::aggregates::group_values::multi_group_by::GroupColumn -/// `GroupValuesColumn`: crate::aggregates::group_values::GroupValuesColumn +/// `GroupValuesColumn`: crate::aggregates::group_values::multi_group_by::GroupValuesColumn /// `GroupValuesRows`: crate::aggregates::group_values::GroupValuesRows pub fn new_group_values( schema: SchemaRef, From 8db894fbce0fdb6f699b77fce9c12bd31fcd7755 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:52:32 -0400 Subject: [PATCH 24/41] fix: slice materialized partial dictionary output safely --- .../aggregates/aggregate_hash_table/common.rs | 171 +++++++++++------- 1 file changed, 106 insertions(+), 65 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs index f500c72b22e07..45db47f978923 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs @@ -28,8 +28,8 @@ use datafusion_physical_expr::aggregate::AggregateFunctionExpr; use crate::PhysicalExpr; use crate::aggregates::group_values::{ - GroupByMetrics, GroupValues, group_value_emit_batch_size, new_group_values, - schema_with_group_values, + GroupByMetrics, GroupValues, cast_group_values_to_schema, + group_value_emit_batch_size, new_group_values, schema_with_group_values, }; use crate::aggregates::grouped_hash_stream::create_group_accumulator; use crate::aggregates::order::GroupOrdering; @@ -208,74 +208,78 @@ impl AggregateHashTable { Ok(()) } - /// Emits one partial-state batch without changing the planned schema. + /// Materializes all partial state once, then returns schema-stable slices. /// - /// Dictionary group columns are capped at the number of values representable - /// by their planned key type. Remaining groups stay in the table and are - /// emitted on later polls. This keeps repartition/coalescing schemas stable - /// while allowing a downstream final aggregate to combine all groups and - /// promote its final output key type when required. + /// A materialized dictionary group array may need a wider key type for all + /// groups. Each returned partial batch is therefore limited to the planned + /// key capacity and re-encoded to the planned schema after slicing. This + /// preserves the existing one-time materialization semantics while keeping + /// repartition and coalescing schemas stable. pub(super) fn next_partial_output_batch_inner( &mut self, materialize_accumulator_fn: MaterializeAccumulatorFn, ) -> Result> { - let output_schema = Arc::clone(&self.output_schema); - let batch_size = self.batch_size; + let planned_schema = Arc::clone(&self.output_schema); + let configured_batch_size = self.batch_size; - let mut state = + let mut output = match std::mem::replace(&mut self.state, AggregateHashTableState::Done) { - AggregateHashTableState::Outputting(state) => state, + AggregateHashTableState::Outputting(mut state) => { + if state.group_values.is_empty() { + return Ok(None); + } + + let emit_to = EmitTo::All; + let timer = self.group_by_metrics.emitting_time.timer(); + let mut columns = state.group_values.emit(emit_to)?; + let num_group_columns = columns.len(); + for acc in state.accumulators.iter_mut() { + columns.extend(materialize_accumulator_fn(acc, emit_to)?); + } + drop(timer); + + let materialized_schema = schema_with_group_values( + &planned_schema, + &columns[..num_group_columns], + ); + let batch = RecordBatch::try_new(materialized_schema, columns)?; + debug_assert!(batch.num_rows() > 0); + let batch_size = group_value_emit_batch_size( + &planned_schema, + num_group_columns, + configured_batch_size, + ); + MaterializedAggregateOutput::new_with_output_schema( + batch, + planned_schema, + num_group_columns, + batch_size, + ) + } + AggregateHashTableState::OutputtingMaterialized(output) => output, AggregateHashTableState::Done => return Ok(None), AggregateHashTableState::Building(_) => { return internal_err!( "next_output_batch must be called in the outputting state" ); } - AggregateHashTableState::OutputtingMaterialized(_) => { - return internal_err!( - "partial aggregate output must not be materialized" - ); - } }; - if state.group_values.is_empty() { - return Ok(None); - } - - let max_groups = group_value_emit_batch_size( - &output_schema, - state.group_by.num_group_exprs(), - batch_size, - ); - let num_groups = max_groups.min(state.group_values.len()); - let emit_to = if num_groups == state.group_values.len() { - EmitTo::All - } else { - EmitTo::First(num_groups) - }; - - let timer = self.group_by_metrics.emitting_time.timer(); - let mut columns = state.group_values.emit(emit_to)?; - for acc in state.accumulators.iter_mut() { - columns.extend(materialize_accumulator_fn(acc, emit_to)?); - } - drop(timer); - - let batch = RecordBatch::try_new(output_schema, columns)?; - debug_assert!(batch.num_rows() > 0); - if state.group_values.is_empty() { + let batch = output.next_batch()?; + if output.is_exhausted() { self.state = AggregateHashTableState::Done; } else { - self.state = AggregateHashTableState::Outputting(state); + self.state = AggregateHashTableState::OutputtingMaterialized(output); } - Ok(Some(batch)) + Ok(batch) } /// Materializes the full output once, then returns it downstream incrementally /// by slicing it into `batch_size` chunks. /// - /// Final aggregate evaluation consumes accumulator state, so it is - /// materialized once and then sliced on later polls. + /// Each aggregation mode chooses a different `materialize_accumulator_fn` + /// according to its semantics. For example, partial aggregation emits + /// partial states to feed the final stage, so it uses [`GroupsAccumulator::state`]. /// /// This is a temporary solution until blocked state management is implemented: /// Issue: @@ -310,7 +314,7 @@ impl AggregateHashTable { ); let batch = RecordBatch::try_new(output_schema, columns)?; debug_assert!(batch.num_rows() > 0); - MaterializedAggregateOutput::new(batch) + MaterializedAggregateOutput::new(batch, batch_size) } AggregateHashTableState::OutputtingMaterialized(output) => output, AggregateHashTableState::Done => return Ok(None), @@ -321,7 +325,7 @@ impl AggregateHashTable { } }; - let batch = output.next_batch(batch_size); + let batch = output.next_batch()?; if output.is_exhausted() { self.state = AggregateHashTableState::Done; } else { @@ -406,7 +410,7 @@ pub(super) type AggregateAccumulator = HashAggregateAccumulator; /// Arguments: /// * accumulator to update. /// * accumulator's evaluated arguments and optional filter. -/// * group index per input row, mapping each row to its interned group. +/// * one group index per input row, mapping each row to its interned group. /// * total number of groups currently interned in that buffer, including newly /// interned groups. pub(super) type AggregateBatchFn = fn( @@ -483,7 +487,7 @@ pub(super) enum AggregateHashTableState { Building(AggregateHashTableBuffer), /// Emitting results directly from group keys and aggregate state. Outputting(AggregateHashTableBuffer), - /// Materialize all the output results, and then incrementally output in the `OutputtingMaterialized` state. + /// Materialized output that is being sliced into downstream batches. /// /// Note this is a temporary solution until the `GroupValues` issue is solved: /// Issue: @@ -493,28 +497,65 @@ pub(super) enum AggregateHashTableState { /// Fully evaluated aggregate output and the next row offset to emit. /// -/// Final aggregate evaluation consumes accumulator state, so it is materialized -/// once and then sliced to honor `batch_size` across output polls. +/// Partial output can use a narrower target schema than the materialized batch; +/// final output uses the materialized schema directly. pub(super) struct MaterializedAggregateOutput { batch: RecordBatch, + output_schema: SchemaRef, + num_group_columns: usize, + batch_size: usize, offset: usize, } impl MaterializedAggregateOutput { - pub(super) fn new(batch: RecordBatch) -> Self { - Self { batch, offset: 0 } + pub(super) fn new(batch: RecordBatch, batch_size: usize) -> Self { + Self { + output_schema: batch.schema(), + batch, + num_group_columns: 0, + batch_size, + offset: 0, + } } - pub(super) fn next_batch(&mut self, batch_size: usize) -> Option { - debug_assert!(batch_size > 0); + pub(super) fn new_with_output_schema( + batch: RecordBatch, + output_schema: SchemaRef, + num_group_columns: usize, + batch_size: usize, + ) -> Self { + Self { + batch, + output_schema, + num_group_columns, + batch_size, + offset: 0, + } + } + + pub(super) fn next_batch(&mut self) -> Result> { + debug_assert!(self.batch_size > 0); if self.is_exhausted() { - return None; + return Ok(None); } - let length = batch_size.min(self.batch.num_rows() - self.offset); + let length = self.batch_size.min(self.batch.num_rows() - self.offset); let batch = self.batch.slice(self.offset, length); self.offset += length; - Some(batch) + + if batch.schema() == self.output_schema { + return Ok(Some(batch)); + } + + let mut columns = batch.columns().to_vec(); + cast_group_values_to_schema( + &mut columns[..self.num_group_columns], + &self.output_schema, + )?; + Ok(Some(RecordBatch::try_new( + Arc::clone(&self.output_schema), + columns, + )?)) } pub(super) fn is_exhausted(&self) -> bool { @@ -699,12 +740,12 @@ mod tests { schema, vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5]))], )?; - let mut output = MaterializedAggregateOutput::new(batch); + let mut output = MaterializedAggregateOutput::new(batch, 2); - assert_eq!(int32_values(&output.next_batch(2).unwrap(), 0), vec![1, 2]); - assert_eq!(int32_values(&output.next_batch(2).unwrap(), 0), vec![3, 4]); - assert_eq!(int32_values(&output.next_batch(2).unwrap(), 0), vec![5]); - assert!(output.next_batch(2).is_none()); + assert_eq!(int32_values(&output.next_batch()?.unwrap(), 0), vec![1, 2]); + assert_eq!(int32_values(&output.next_batch()?.unwrap(), 0), vec![3, 4]); + assert_eq!(int32_values(&output.next_batch()?.unwrap(), 0), vec![5]); + assert!(output.next_batch()?.is_none()); assert!(output.is_exhausted()); Ok(()) From fe31614203b7c68b4bc0436b44bb64967fa94ea3 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:53:47 -0400 Subject: [PATCH 25/41] fix: re-encode sliced dictionary transport batches --- .../src/aggregates/group_values/mod.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/group_values/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/mod.rs index 97409dfb69a0b..6888f841e7deb 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/mod.rs @@ -105,16 +105,24 @@ fn dictionary_key_capacity(key_type: &DataType) -> Option { } /// Casts the leading group-value arrays to the corresponding schema fields. -/// This is used by spill files, which require one stable IPC schema even though -/// normal aggregate output may promote dictionary keys dynamically. +/// Dictionary arrays are decoded and re-encoded when their key type changes so +/// sliced batches receive compact keys starting at zero. pub(crate) fn cast_group_values_to_schema( group_values: &mut [ArrayRef], schema: &SchemaRef, ) -> Result<()> { for (array, field) in group_values.iter_mut().zip(schema.fields()) { - if array.data_type() != field.data_type() { - *array = cast(array.as_ref(), field.data_type())?; + if array.data_type() == field.data_type() { + continue; } + + *array = match (array.data_type(), field.data_type()) { + (DataType::Dictionary(_, _), DataType::Dictionary(_, value_type)) => { + let values = cast(array.as_ref(), value_type.as_ref())?; + cast(values.as_ref(), field.data_type())? + } + _ => cast(array.as_ref(), field.data_type())?, + }; } Ok(()) } From f13a8d0b9edb4e00b49c95bbb2175f84d277c4fe Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 12:59:49 -0400 Subject: [PATCH 26/41] ci: validate dictionary group key promotion --- .github/workflows/validate-df23127.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/validate-df23127.yml diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml new file mode 100644 index 0000000000000..743812b86bddf --- /dev/null +++ b/.github/workflows/validate-df23127.yml @@ -0,0 +1,26 @@ +name: Validate DataFusion 23127 + +on: + push: + branches: + - validate/df-23127-20260710 + +permissions: + contents: read + +jobs: + targeted-tests: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - name: Show toolchain + run: rustc --version && cargo --version + - name: Check formatting + run: cargo fmt --check + - name: Test partial and final dictionary transport + run: cargo test -p datafusion-physical-plan dictionary_groups_keep_partial_schema_and_promote_final_output -- --nocapture + - name: Test monotonic dictionary promotion + run: cargo test -p datafusion-physical-plan dict_uint8_utf8_promotes_and_does_not_shrink -- --nocapture + - name: Test legacy single-stage promotion + run: cargo test -p datafusion-physical-plan dictionary_group_key_promotes_runtime_schema -- --nocapture From e4a812f97c5013997b9518bf57e0540f0cc76484 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:00:47 -0400 Subject: [PATCH 27/41] ci: run targeted validation on fork pull request --- .github/workflows/validate-df23127.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml index 743812b86bddf..3430600963224 100644 --- a/.github/workflows/validate-df23127.yml +++ b/.github/workflows/validate-df23127.yml @@ -1,9 +1,9 @@ name: Validate DataFusion 23127 on: - push: + pull_request: branches: - - validate/df-23127-20260710 + - main permissions: contents: read From ae18069918cda27a03ffbbd900e02d03594b6f48 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:02:24 -0400 Subject: [PATCH 28/41] ci: capture rustfmt patch and continue targeted tests --- .github/workflows/validate-df23127.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml index 3430600963224..1c79f28f8467d 100644 --- a/.github/workflows/validate-df23127.yml +++ b/.github/workflows/validate-df23127.yml @@ -16,8 +16,16 @@ jobs: - uses: actions/checkout@v4 - name: Show toolchain run: rustc --version && cargo --version - - name: Check formatting - run: cargo fmt --check + - name: Format disposable checkout + run: | + cargo fmt + git diff --binary > /tmp/rustfmt.patch + git diff --check + - name: Upload rustfmt patch + uses: actions/upload-artifact@v4 + with: + name: rustfmt-patch + path: /tmp/rustfmt.patch - name: Test partial and final dictionary transport run: cargo test -p datafusion-physical-plan dictionary_groups_keep_partial_schema_and_promote_final_output -- --nocapture - name: Test monotonic dictionary promotion From de433418cf9830818e88f930b83ffe0e5a5c0682 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:05:49 -0400 Subject: [PATCH 29/41] ci: persist rustfmt output on validation branch --- .github/workflows/validate-df23127.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml index 1c79f28f8467d..cc1ff2caaec83 100644 --- a/.github/workflows/validate-df23127.yml +++ b/.github/workflows/validate-df23127.yml @@ -6,7 +6,7 @@ on: - main permissions: - contents: read + contents: write jobs: targeted-tests: @@ -14,18 +14,23 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@v4 + with: + ref: validate/df-23127-20260710 - name: Show toolchain run: rustc --version && cargo --version - - name: Format disposable checkout + - name: Format validation branch run: | cargo fmt - git diff --binary > /tmp/rustfmt.patch git diff --check - - name: Upload rustfmt patch - uses: actions/upload-artifact@v4 - with: - name: rustfmt-patch - path: /tmp/rustfmt.patch + - name: Commit rustfmt output + run: | + if ! git diff --quiet; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add datafusion + git commit -m "style: apply rustfmt" + git push origin HEAD:validate/df-23127-20260710 + fi - name: Test partial and final dictionary transport run: cargo test -p datafusion-physical-plan dictionary_groups_keep_partial_schema_and_promote_final_output -- --nocapture - name: Test monotonic dictionary promotion From 587c61ad169175f2b819aaa8469017a6a632d839 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:06:32 +0000 Subject: [PATCH 30/41] style: apply rustfmt --- .../aggregate_hash_table/common_ordered.rs | 12 ++--- .../aggregates/aggregate_hash_table/tests.rs | 16 +++--- .../src/aggregates/group_values/mod.rs | 15 +++--- .../src/aggregates/group_values/row.rs | 29 +++++------ .../src/aggregates/grouped_hash_stream.rs | 51 +++++++++---------- 5 files changed, 56 insertions(+), 67 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs index 855568fa39aa0..fa77f7a4d6e4f 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/common_ordered.rs @@ -339,11 +339,8 @@ impl OrderedAggregateTable { self.batch_size, ) }; - let (emit_to, should_remove_groups) = self.clamp_emit_to( - self.buffer.group_values.len(), - emit_to, - max_batch_size, - ); + let (emit_to, should_remove_groups) = + self.clamp_emit_to(self.buffer.group_values.len(), emit_to, max_batch_size); let timer = self.group_by_metrics.emitting_time.timer(); let mut output = self.buffer.group_values.emit(emit_to)?; @@ -368,10 +365,7 @@ impl OrderedAggregateTable { drop(timer); let output_schema = if is_final { - schema_with_group_values( - &self.output_schema, - &output[..num_group_columns], - ) + schema_with_group_values(&self.output_schema, &output[..num_group_columns]) } else { Arc::clone(&self.output_schema) }; diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs index 98eeb6cdd7382..5e22f538a6233 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs @@ -17,9 +17,7 @@ use std::sync::Arc; -use arrow::array::{ - ArrayRef, DictionaryArray, StringArray, StringDictionaryBuilder, -}; +use arrow::array::{ArrayRef, DictionaryArray, StringArray, StringDictionaryBuilder}; use arrow::datatypes::{DataType, Field, Schema, UInt8Type, UInt16Type}; use arrow::record_batch::RecordBatch; use datafusion_common::Result; @@ -86,11 +84,13 @@ fn dictionary_groups_keep_partial_schema_and_promote_final_output() -> Result<() batch.schema().field(0).data_type(), &DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)) ); - assert!(batch - .column(0) - .as_any() - .downcast_ref::>() - .is_some()); + assert!( + batch + .column(0) + .as_any() + .downcast_ref::>() + .is_some() + ); partial_batches.push(batch); } assert_eq!( diff --git a/datafusion/physical-plan/src/aggregates/group_values/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/mod.rs index 6888f841e7deb..ef9fd54776025 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/mod.rs @@ -60,10 +60,7 @@ pub(crate) fn schema_with_group_values( }) .collect::>(); - Arc::new(Schema::new_with_metadata( - fields, - schema.metadata().clone(), - )) + Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone())) } /// Returns the largest partial aggregate batch that can be emitted without @@ -147,10 +144,7 @@ pub(crate) fn group_value_spill_schema( }) .collect::>(); - Arc::new(Schema::new_with_metadata( - fields, - schema.metadata().clone(), - )) + Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone())) } fn group_value_spill_field(field: &Field) -> FieldRef { @@ -166,7 +160,10 @@ fn group_value_spill_field(field: &Field) -> FieldRef { | DataType::UInt64 => DataType::UInt64, _ => key_type.as_ref().clone(), }; - DataType::Dictionary(Box::new(key_type), Box::new(value_type.as_ref().clone())) + DataType::Dictionary( + Box::new(key_type), + Box::new(value_type.as_ref().clone()), + ) } _ => field.data_type().clone(), }; diff --git a/datafusion/physical-plan/src/aggregates/group_values/row.rs b/datafusion/physical-plan/src/aggregates/group_values/row.rs index 2aecd4cd1d53a..c688a40f85699 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/row.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/row.rs @@ -302,10 +302,7 @@ fn materialized_row_schema(schema: &SchemaRef) -> SchemaRef { _ => Arc::clone(field), }) .collect::>(); - Arc::new(Schema::new_with_metadata( - fields, - schema.metadata().clone(), - )) + Arc::new(Schema::new_with_metadata(fields, schema.metadata().clone())) } fn dictionary_encode_with_promotion( @@ -421,9 +418,7 @@ fn promote_dictionary_type(data_type: &DataType) -> Option { mod tests { use super::*; use arrow::array::{DictionaryArray, StringArray, StringDictionaryBuilder}; - use arrow::datatypes::{ - Field, Int8Type, Int16Type, Schema, UInt8Type, UInt16Type, - }; + use arrow::datatypes::{Field, Int8Type, Int16Type, Schema, UInt8Type, UInt16Type}; #[test] fn dict_uint8_utf8_promotes_and_does_not_shrink() -> Result<()> { @@ -493,10 +488,12 @@ mod tests { arrays[0].data_type(), &DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)) ); - assert!(arrays[0] - .as_any() - .downcast_ref::>() - .is_some()); + assert!( + arrays[0] + .as_any() + .downcast_ref::>() + .is_some() + ); Ok(()) } @@ -520,10 +517,12 @@ mod tests { arrays[0].data_type(), &DataType::Dictionary(Box::new(DataType::Int16), Box::new(DataType::Utf8)) ); - assert!(arrays[0] - .as_any() - .downcast_ref::>() - .is_some()); + assert!( + arrays[0] + .as_any() + .downcast_ref::>() + .is_some() + ); Ok(()) } } diff --git a/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs b/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs index 83ab3f570a542..8cf5531b2507b 100644 --- a/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs +++ b/datafusion/physical-plan/src/aggregates/grouped_hash_stream.rs @@ -1039,24 +1039,23 @@ impl GroupedHashAggregateStream { return Ok(None); } - let emit_to = if !spilling - && self.mode.output_mode() == AggregateOutputMode::Partial - { - let max_groups = group_value_emit_batch_size( - &self.schema, - self.group_by.num_group_exprs(), - self.batch_size, - ); - match emit_to { - EmitTo::First(n) => EmitTo::First(n.min(max_groups)), - EmitTo::All if self.group_values.len() > max_groups => { - EmitTo::First(max_groups) + let emit_to = + if !spilling && self.mode.output_mode() == AggregateOutputMode::Partial { + let max_groups = group_value_emit_batch_size( + &self.schema, + self.group_by.num_group_exprs(), + self.batch_size, + ); + match emit_to { + EmitTo::First(n) => EmitTo::First(n.min(max_groups)), + EmitTo::All if self.group_values.len() > max_groups => { + EmitTo::First(max_groups) + } + EmitTo::All => EmitTo::All, } - EmitTo::All => EmitTo::All, - } - } else { - emit_to - }; + } else { + emit_to + }; let timer = self.group_by_metrics.emitting_time.timer(); let mut output = self.group_values.emit(emit_to)?; @@ -1070,10 +1069,8 @@ impl GroupedHashAggregateStream { } else if self.mode.output_mode() == AggregateOutputMode::Partial { Arc::clone(&self.schema) } else { - let schema = schema_with_group_values( - &self.schema, - &output[..num_group_columns], - ); + let schema = + schema_with_group_values(&self.schema, &output[..num_group_columns]); self.schema = Arc::clone(&schema); schema }; @@ -1498,11 +1495,13 @@ mod tests { batch.schema().field(0).data_type(), &DataType::Dictionary(Box::new(DataType::UInt16), Box::new(DataType::Utf8)) ); - assert!(batch - .column(0) - .as_any() - .downcast_ref::>() - .is_some()); + assert!( + batch + .column(0) + .as_any() + .downcast_ref::>() + .is_some() + ); assert!(stream.next().await.is_none()); Ok(()) } From bdb1f48b968e58d54f47e86fcbef00a5077cf578 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:11:23 -0400 Subject: [PATCH 31/41] ci: add SQL regression and clippy validation --- .github/workflows/validate-df23127.yml | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml index cc1ff2caaec83..d2a02cdf140e4 100644 --- a/.github/workflows/validate-df23127.yml +++ b/.github/workflows/validate-df23127.yml @@ -6,7 +6,7 @@ on: - main permissions: - contents: write + contents: read jobs: targeted-tests: @@ -18,22 +18,15 @@ jobs: ref: validate/df-23127-20260710 - name: Show toolchain run: rustc --version && cargo --version - - name: Format validation branch - run: | - cargo fmt - git diff --check - - name: Commit rustfmt output - run: | - if ! git diff --quiet; then - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add datafusion - git commit -m "style: apply rustfmt" - git push origin HEAD:validate/df-23127-20260710 - fi + - name: Check formatting + run: cargo fmt --check - name: Test partial and final dictionary transport run: cargo test -p datafusion-physical-plan dictionary_groups_keep_partial_schema_and_promote_final_output -- --nocapture - name: Test monotonic dictionary promotion run: cargo test -p datafusion-physical-plan dict_uint8_utf8_promotes_and_does_not_shrink -- --nocapture - name: Test legacy single-stage promotion run: cargo test -p datafusion-physical-plan dictionary_group_key_promotes_runtime_schema -- --nocapture + - name: Test SQL dictionary promotion + run: cargo test --test sqllogictests -- dictionary_group_by_key_promotion + - name: Run clippy + run: cargo clippy -p datafusion-physical-plan --all-targets -- -D warnings From dc3f7a2f124a9d24adb73b4265f3e06989656a16 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:23:47 -0400 Subject: [PATCH 32/41] ci: persist SQL failure and run clippy independently --- .github/workflows/validate-df23127.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml index d2a02cdf140e4..c00bfcec857e8 100644 --- a/.github/workflows/validate-df23127.yml +++ b/.github/workflows/validate-df23127.yml @@ -6,7 +6,7 @@ on: - main permissions: - contents: read + contents: write jobs: targeted-tests: @@ -27,6 +27,23 @@ jobs: - name: Test legacy single-stage promotion run: cargo test -p datafusion-physical-plan dictionary_group_key_promotes_runtime_schema -- --nocapture - name: Test SQL dictionary promotion - run: cargo test --test sqllogictests -- dictionary_group_by_key_promotion + id: sql + continue-on-error: true + run: | + set -o pipefail + cargo test --test sqllogictests -- dictionary_group_by_key_promotion 2>&1 | tee /tmp/df23127-sql.log + - name: Persist SQL failure log + if: steps.sql.outcome == 'failure' + run: | + mkdir -p validation + cp /tmp/df23127-sql.log validation/df23127-sql.log + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add validation/df23127-sql.log + git commit -m "ci: capture df23127 SQL failure [skip ci]" + git push origin HEAD:validate/df-23127-20260710 - name: Run clippy run: cargo clippy -p datafusion-physical-plan --all-targets -- -D warnings + - name: Fail when SQL regression fails + if: steps.sql.outcome == 'failure' + run: exit 1 From a0110108ee47bc9592f2104a2ab2e587ce3f6c21 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:32:35 +0000 Subject: [PATCH 33/41] ci: capture df23127 SQL failure [skip ci] --- validation/df23127-sql.log | 541 +++++++++++++++++++++++++++++++++++++ 1 file changed, 541 insertions(+) create mode 100644 validation/df23127-sql.log diff --git a/validation/df23127-sql.log b/validation/df23127-sql.log new file mode 100644 index 0000000000000..759a78fb76fb0 --- /dev/null +++ b/validation/df23127-sql.log @@ -0,0 +1,541 @@ + Downloading crates ... + Downloaded anstyle-query v1.1.5 + Downloaded cfg_aliases v0.2.1 + Downloaded enum-ordinalize v4.3.2 + Downloaded num-iter v0.1.45 + Downloaded ref-cast-impl v1.0.25 + Downloaded openssl-probe v0.2.1 + Downloaded sha1 v0.11.0 + Downloaded structmeta-derive v0.3.0 + Downloaded typify v0.5.0 + Downloaded wasm-bindgen-macro v0.2.118 + Downloaded wasm-bindgen-test-macro v0.3.68 + Downloaded xmlparser v0.13.6 + Downloaded tower-service v0.3.3 + Downloaded unit-prefix v0.5.2 + Downloaded wasm-bindgen-shared v0.2.118 + Downloaded zeroize v1.8.2 + Downloaded sharded-slab v0.1.7 + Downloaded structmeta v0.3.0 + Downloaded utf8-zero v0.8.1 + Downloaded sysinfo v0.39.5 + Downloaded wasm-bindgen v0.2.118 + Downloaded yansi v1.0.1 + Downloaded xattr v1.6.1 + Downloaded typenum v1.20.0 + Downloaded rustc-hash v2.1.2 + Downloaded try-lock v0.2.5 + Downloaded wasm-bindgen-test-shared v0.2.118 + Downloaded tracing-subscriber v0.3.23 + Downloaded quinn-proto v0.11.15 + Downloaded sha1 v0.10.6 + Downloaded powerfmt v0.2.0 + Downloaded time-core v0.1.8 + Downloaded time v0.3.47 + Downloaded stabby-macros v72.1.8 + Downloaded ureq v3.3.0 + Downloaded libmimalloc-sys v0.1.49 + Downloaded zlib-rs v0.6.3 + Downloaded regex-lite v0.1.9 + Downloaded wasm-bindgen-futures v0.4.68 + Downloaded testcontainers-modules v0.15.0 + Downloaded recursive-proc-macro-impl v0.1.1 + Downloaded libbz2-rs-sys v0.2.3 + Downloaded quick-xml v0.39.2 + Downloaded pin-utils v0.1.0 + Downloaded hyper-timeout v0.5.2 + Downloaded tower-http v0.6.8 + Downloaded pbjson-build v0.8.0 + Downloaded parse-display-derive v0.9.1 + Downloaded parquet v59.1.0 + Downloaded outref v0.5.2 + Downloaded testcontainers v0.27.3 + Downloaded prost-build v0.14.4 + Downloaded nix v0.31.3 + Downloaded lazy_static v1.5.0 + Downloaded stabby-abi v72.1.8 + Downloaded ref-cast v1.0.25 + Downloaded typify-impl v0.5.0 + Downloaded libloading v0.9.0 + Downloaded ureq-proto v0.6.0 + Downloaded untrusted v0.9.0 + Downloaded regress v0.10.5 + Downloaded object v0.37.3 + Downloaded hashbrown v0.12.3 + Downloaded urlencoding v2.1.3 + Downloaded tracing-log v0.2.0 + Downloaded tower-layer v0.3.3 + Downloaded recursive v0.1.1 + Downloaded js-sys v0.3.95 + Downloaded ipnet v2.12.0 + Downloaded insta-cmd v0.7.0 + Downloaded utf8parse v0.2.2 + Downloaded tokio-rustls v0.26.4 + Downloaded thread_local v1.1.9 + Downloaded sqlparser v0.62.0 + Downloaded sha2-const-stable v0.1.0 + Downloaded sha2 v0.11.0 + Downloaded paste v1.0.15 + Downloaded hyper-util v0.1.20 + Downloaded subtle v2.6.1 + Downloaded strsim v0.11.1 + Downloaded rand v0.10.1 + Downloaded prost v0.14.4 + Downloaded pretty_assertions v1.4.1 + Downloaded pbjson-build v0.9.0 + Downloaded libtest-mimic v0.8.2 + Downloaded hybrid-array v0.4.11 + Downloaded unicode-width v0.1.14 + Downloaded tinyvec_macros v0.1.1 + Downloaded nom v8.0.0 + Downloaded multimap v0.10.1 + Downloaded md-5 v0.11.0 + Downloaded matchit v0.8.4 + Downloaded iri-string v0.7.12 + Downloaded heck v0.5.0 + Downloaded option-ext v0.2.0 + Downloaded indexmap v1.9.3 + Downloaded httpdate v1.0.3 + Downloaded hashbrown v0.16.1 + Downloaded wasm-bindgen-test v0.3.68 + Downloaded unsafe-libyaml v0.2.11 + Downloaded typify-macro v0.5.0 + Downloaded tower v0.5.3 + Downloaded serde_with v3.18.0 + Downloaded mio v1.2.0 + Downloaded wasm-bindgen-macro-support v0.2.118 + Downloaded tonic v0.14.6 + Downloaded tinyvec v1.11.0 + Downloaded strum v0.28.0 + Downloaded socket2 v0.6.3 + Downloaded snap v1.1.1 + Downloaded serde_tokenstream v0.2.3 + Downloaded sqllogictest v0.29.1 + Downloaded signal-hook-registry v1.4.8 + Downloaded schemars_derive v0.8.22 + Downloaded schemars v1.2.1 + Downloaded schemars v0.9.0 + Downloaded schemars v0.8.22 + Downloaded fnv v1.0.7 + Downloaded tonic-prost v0.14.5 + Downloaded tokio-stream v0.1.18 + Downloaded time-macros v0.2.27 + Downloaded substrait v0.63.0 + Downloaded strum_macros v0.28.0 + Downloaded serde_repr v0.1.20 + Downloaded rustversion v1.0.22 + Downloaded httparse v1.10.1 + Downloaded hmac v0.13.0 + Downloaded fs-err v3.3.0 + Downloaded brotli v8.0.2 + Downloaded vsimd v0.8.0 + Downloaded ring v0.17.14 + Downloaded http v0.2.12 + Downloaded generic-array v0.14.7 + Downloaded enum-ordinalize-derive v4.3.2 + Downloaded aws-smithy-types v1.4.9 + Downloaded stacker v0.1.24 + Downloaded stabby v72.1.8 + Downloaded simd-adler32 v0.3.9 + Downloaded serde_yaml v0.9.34+deprecated + Downloaded rustls-webpki v0.103.13 + Downloaded rustls-pki-types v1.14.1 + Downloaded portable-atomic v1.13.1 + Downloaded pbjson v0.8.0 + Downloaded md-5 v0.10.6 + Downloaded lru-slab v0.1.2 + Downloaded linktime-proc-macro v0.2.0 + Downloaded liblzma-sys v0.4.6 + Downloaded deranged v0.5.8 + Downloaded arrow-avro v59.1.0 + Downloaded want v0.3.1 + Downloaded serde_with_macros v3.18.0 + Downloaded serde_urlencoded v0.7.1 + Downloaded rustyline v18.0.1 + Downloaded reqwest v0.12.28 + Downloaded rand_distr v0.5.1 + Downloaded quinn-udp v0.5.14 + Downloaded psm v0.1.31 + Downloaded prost-derive v0.14.4 + Downloaded prettyplease v0.2.37 + Downloaded parse-display v0.9.1 + Downloaded num-rational v0.4.2 + Downloaded num-conv v0.2.1 + Downloaded liblzma v0.4.7 + Downloaded jiff v0.2.24 + Downloaded cmov v0.5.4 + Downloaded chacha20 v0.10.0 + Downloaded brotli-decompressor v5.0.0 + Downloaded bollard-buildkit-proto v0.7.0 + Downloaded bollard v0.20.2 + Downloaded bigdecimal v0.4.10 + Downloaded axum-core v0.5.6 + Downloaded aws-smithy-runtime v1.11.3 + Downloaded sync_wrapper v1.0.2 + Downloaded subst v0.3.8 + Downloaded seq-macro v0.3.6 + Downloaded rustls-native-certs v0.8.3 + Downloaded num v0.4.3 + Downloaded miniz_oxide v0.8.9 + Downloaded mime v0.3.17 + Downloaded is_terminal_polyfill v1.70.2 + Downloaded hyper v1.9.0 + Downloaded home v0.5.12 + Downloaded h2 v0.4.13 + Downloaded flate2 v1.1.9 + Downloaded escape8259 v0.5.3 + Downloaded env_filter v2.0.0 + Downloaded dunce v1.0.5 + Downloaded dirs-sys v0.5.0 + Downloaded digest v0.11.2 + Downloaded digest v0.10.7 + Downloaded diff v0.1.13 + Downloaded crc v3.4.0 + Downloaded constant_time_eq v0.4.2 + Downloaded block-buffer v0.10.4 + Downloaded sqlparser_derive v0.5.0 + Downloaded rustls v0.23.39 + Downloaded radix_trie v0.3.0 + Downloaded quinn v0.11.11 + Downloaded pbjson-types v0.8.0 + Downloaded owo-colors v4.3.0 + Downloaded ident_case v1.0.1 + Downloaded http-body-util v0.1.3 + Downloaded http-body v0.4.6 + Downloaded etcetera v0.11.0 + Downloaded docker_credential v1.3.2 + Downloaded dirs v6.0.0 + Downloaded darling v0.23.0 + Downloaded ctutils v0.4.2 + Downloaded crypto-common v0.2.1 + Downloaded crypto-common v0.1.7 + Downloaded crc-catalog v2.5.0 + Downloaded compression-core v0.4.32 + Downloaded cmake v0.1.58 + Downloaded bytes-utils v0.1.4 + Downloaded base64 v0.21.7 + Downloaded aws-smithy-xml v0.60.15 + Downloaded serde_derive_internals v0.29.1 + Downloaded rand_core v0.10.1 + Downloaded prost-types v0.14.4 + Downloaded link-section v0.18.1 + Downloaded indicatif v0.18.5 + Downloaded fs_extra v1.3.0 + Downloaded educe v0.6.0 + Downloaded dyn-clone v1.0.20 + Downloaded darling_macro v0.23.0 + Downloaded darling_core v0.23.0 + Downloaded cpufeatures v0.2.17 + Downloaded const-oid v0.10.2 + Downloaded colorchoice v1.0.5 + Downloaded clap_derive v4.6.1 + Downloaded bzip2 v0.6.1 + Downloaded blake3 v1.8.5 + Downloaded axum v0.8.9 + Downloaded aws-types v1.3.16 + Downloaded aws-smithy-schema v0.1.0 + Downloaded aws-smithy-runtime-api-macros v1.0.0 + Downloaded aws-smithy-query v0.60.15 + Downloaded aws-smithy-json v0.62.7 + Downloaded aws-smithy-http-client v1.1.12 + Downloaded aws-sdk-sts v1.106.0 + Downloaded aws-sdk-ssooidc v1.103.0 + Downloaded aws-sdk-sso v1.101.0 + Downloaded aws-runtime v1.7.4 + Downloaded aws-lc-rs v1.16.3 + Downloaded aws-config v1.8.18 + Downloaded async-compression v0.4.42 + Downloaded astral-tokio-tar v0.6.2 + Downloaded arrow-flight v59.1.0 + Downloaded anyhow v1.0.103 + Downloaded nu-ansi-term v0.50.3 + Downloaded endian-type v0.2.0 + Downloaded bumpalo v3.20.2 + Downloaded aws-smithy-runtime-api v1.12.3 + Downloaded aws-smithy-observability v0.2.6 + Downloaded aws-sigv4 v1.4.4 + Downloaded async-ffi v0.5.0 + Downloaded arrayvec v0.7.6 + Downloaded arrayref v0.3.9 + Downloaded arc-swap v1.9.1 + Downloaded ar_archive_writer v0.5.1 + Downloaded anstyle-parse v1.0.0 + Downloaded mimalloc v0.1.52 + Downloaded hyper-rustls v0.27.9 + Downloaded filetime v0.2.27 + Downloaded ferroid v2.0.0 + Downloaded ctor v1.0.7 + Downloaded crc32fast v1.5.0 + Downloaded cpufeatures v0.3.0 + Downloaded bollard-stubs v1.52.1-rc.29.1.3 + Downloaded blake2 v0.10.6 + Downloaded aws-smithy-async v1.2.14 + Downloaded aws-credential-types v1.2.14 + Downloaded async-stream-impl v0.3.6 + Downloaded nibble_vec v0.1.0 + Downloaded hyperlocal v0.9.1 + Downloaded http-body v1.0.1 + Downloaded env_logger v0.11.11 + Downloaded doc-comment v0.3.4 + Downloaded compression-codecs v0.4.38 + Downloaded block-buffer v0.12.0 + Downloaded async-stream v0.3.6 + Downloaded anstream v1.0.0 + Downloaded alloc-stdlib v0.2.2 + Downloaded alloc-no-stdlib v2.0.4 + Downloaded base64-simd v0.8.0 + Downloaded aws-smithy-http v0.63.6 + Downloaded atomic-waker v1.1.2 + Downloaded async-recursion v1.1.1 + Downloaded adler2 v2.0.1 + Downloaded aws-lc-sys v0.40.0 + Compiling libc v0.2.186 + Compiling indexmap v2.14.0 + Compiling getrandom v0.3.4 + Compiling smallvec v1.15.1 + Compiling serde_json v1.0.150 + Compiling num-integer v0.1.46 + Compiling num-bigint v0.4.6 + Compiling jobserver v0.1.34 + Compiling cc v1.2.61 + Compiling rand_core v0.9.5 + Compiling rand_chacha v0.9.0 + Compiling parking_lot_core v0.9.12 + Compiling rand v0.9.4 + Compiling errno v0.3.14 + Compiling typenum v1.20.0 + Compiling chrono v0.4.45 + Compiling rand_distr v0.5.1 + Compiling signal-hook-registry v1.4.8 + Compiling parking_lot v0.12.5 + Compiling half v2.7.1 + Compiling socket2 v0.6.3 + Compiling mio v1.2.0 + Compiling tokio v1.52.3 + Compiling arrow-buffer v59.1.0 + Compiling arrow-schema v59.1.0 + Compiling slab v0.4.12 + Compiling chrono-tz v0.10.4 + Compiling arrow-data v59.1.0 + Compiling ahash v0.8.12 + Compiling cmake v0.1.58 + Compiling futures-channel v0.3.32 + Compiling fs_extra v1.3.0 + Compiling dunce v1.0.5 + Compiling aws-lc-sys v0.40.0 + Compiling getrandom v0.2.17 + Compiling futures-util v0.3.32 + Compiling arrow-array v59.1.0 + Compiling zstd-sys v2.0.16+zstd.1.5.7 + Compiling tracing-core v0.1.36 + Compiling tracing v0.1.44 + Compiling tokio-util v0.7.18 + Compiling ring v0.17.14 + Compiling generic-array v0.14.7 + Compiling subtle v2.6.1 + Compiling log v0.4.33 + Compiling aws-lc-rs v1.16.3 + Compiling zeroize v1.8.2 + Compiling serde_core v1.0.228 + Compiling rand_core v0.10.1 + Compiling object v0.37.3 + Compiling getrandom v0.4.2 + Compiling rustls-pki-types v1.14.1 + Compiling arrow-select v59.1.0 + Compiling http-body v1.0.1 + Compiling untrusted v0.9.0 + Compiling httparse v1.10.1 + Compiling zstd-safe v7.2.4 + Compiling semver v1.0.28 + Compiling tower-service v0.3.3 + Compiling try-lock v0.2.5 + Compiling atomic-waker v1.1.2 + Compiling cpufeatures v0.3.0 + Compiling rustls v0.23.39 + Compiling crc32fast v1.5.0 + Compiling fnv v1.0.7 + Compiling h2 v0.4.13 + Compiling want v0.3.1 + Compiling icu_normalizer v2.2.0 + Compiling rustc_version v0.4.1 + Compiling zstd v0.13.3 + Compiling ar_archive_writer v0.5.1 + Compiling block-buffer v0.10.4 + Compiling crypto-common v0.1.7 + Compiling httpdate v1.0.3 + Compiling either v1.15.0 + Compiling adler2 v2.0.1 + Compiling simd-adler32 v0.3.9 + Compiling miniz_oxide v0.8.9 + Compiling hyper v1.9.0 + Compiling digest v0.10.7 + Compiling psm v0.1.31 + Compiling flatbuffers v25.12.19 + Compiling idna_adapter v1.2.1 + Compiling form_urlencoded v1.2.2 + Compiling sync_wrapper v1.0.2 + Compiling openssl-probe v0.2.1 + Compiling zlib-rs v0.6.3 + Compiling tower-layer v0.3.3 + Compiling ipnet v2.12.0 + Compiling hyper-util v0.1.20 + Compiling tower v0.5.3 + Compiling rustls-native-certs v0.8.3 + Compiling idna v1.1.0 + Compiling arrow-ord v59.1.0 + Compiling futures-executor v0.3.32 + Compiling stacker v0.1.24 + Compiling twox-hash v2.1.2 + Compiling snap v1.1.1 + Compiling flate2 v1.1.9 + Compiling iri-string v0.7.12 + Compiling alloc-no-stdlib v2.0.4 + Compiling alloc-stdlib v0.2.2 + Compiling arrow-cast v59.1.0 + Compiling tower-http v0.6.8 + Compiling lz4_flex v0.13.0 + Compiling futures v0.3.32 + Compiling url v2.5.8 + Compiling serde_urlencoded v0.7.1 + Compiling md-5 v0.10.6 + Compiling chacha20 v0.10.0 + Compiling http-body-util v0.1.3 + Compiling paste v1.0.15 + Compiling rand v0.10.1 + Compiling arrow-ipc v59.1.0 + Compiling brotli-decompressor v5.0.0 + Compiling itertools v0.14.0 + Compiling quick-xml v0.39.2 + Compiling recursive-proc-macro-impl v0.1.1 + Compiling anyhow v1.0.103 + Compiling recursive v0.1.1 + Compiling brotli v8.0.2 + Compiling arrow-csv v59.1.0 + Compiling arrow-json v59.1.0 + Compiling arrow-string v59.1.0 + Compiling uuid v1.23.4 + Compiling arrow-arith v59.1.0 + Compiling arrow-row v59.1.0 + Compiling sqlparser_derive v0.5.0 + Compiling seq-macro v0.3.6 + Compiling sqlparser v0.62.0 + Compiling arrow v59.1.0 + Compiling itertools v0.15.0 + Compiling prost-derive v0.14.4 + Compiling prost v0.14.4 + Compiling hybrid-array v0.4.11 + Compiling cmov v0.5.4 + Compiling ctutils v0.4.2 + Compiling const-oid v0.10.2 + Compiling block-buffer v0.12.0 + Compiling crypto-common v0.2.1 + Compiling tempfile v3.27.0 + Compiling petgraph v0.8.3 + Compiling digest v0.11.2 + Compiling dashmap v6.2.1 + Compiling blake3 v1.8.5 + Compiling arrayvec v0.7.6 + Compiling arrayref v0.3.9 + Compiling constant_time_eq v0.4.2 + Compiling md-5 v0.11.0 + Compiling sha2 v0.11.0 + Compiling blake2 v0.10.6 + Compiling liblzma-sys v0.4.6 + Compiling libbz2-rs-sys v0.2.3 + Compiling bzip2 v0.6.1 + Compiling datafusion-common-runtime v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/common-runtime) + Compiling compression-core v0.4.32 + Compiling glob v0.3.3 + Compiling heck v0.5.0 + Compiling utf8parse v0.2.2 + Compiling anstyle-parse v1.0.0 + Compiling is_terminal_polyfill v1.70.2 + Compiling colorchoice v1.0.5 + Compiling anstyle-query v1.1.5 + Compiling anstream v1.0.0 + Compiling bigdecimal v0.4.10 + Compiling crc-catalog v2.5.0 + Compiling crc v3.4.0 + Compiling strum_macros v0.28.0 + Compiling strsim v0.11.1 + Compiling clap_builder v4.6.0 + Compiling liblzma v0.4.7 + Compiling compression-codecs v0.4.38 + Compiling async-compression v0.4.42 + Compiling arrow-avro v59.1.0 + Compiling clap_derive v4.6.1 + Compiling tokio-stream v0.1.18 + Compiling rand_core v0.6.4 + Compiling fs-err v3.3.0 + Compiling enum-ordinalize-derive v4.3.2 + Compiling portable-atomic v1.13.1 + Compiling owo-colors v4.3.0 + Compiling enum-ordinalize v4.3.2 + Compiling rand_chacha v0.3.1 + Compiling clap v4.6.1 + Compiling escape8259 v0.5.3 + Compiling unicode-width v0.1.14 + Compiling subst v0.3.8 + Compiling libtest-mimic v0.8.2 + Compiling rand v0.8.6 + Compiling educe v0.6.0 + Compiling sha1 v0.11.0 + Compiling itertools v0.13.0 + Compiling console v0.16.3 + Compiling unit-prefix v0.5.2 + Compiling indicatif v0.18.5 + Compiling env_filter v2.0.0 + Compiling jiff v0.2.24 + Compiling sqllogictest v0.29.1 + Compiling env_logger v0.11.11 + Compiling rustls-webpki v0.103.13 + Compiling tokio-rustls v0.26.4 + Compiling hyper-rustls v0.27.9 + Compiling reqwest v0.12.28 + Compiling object_store v0.13.2 + Compiling parquet v59.1.0 + Compiling datafusion-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/common) + Compiling datafusion-proto-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/proto-common) + Compiling datafusion-expr-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/expr-common) + Compiling datafusion-proto-models v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/proto-models) + Compiling datafusion-physical-expr-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-expr-common) + Compiling datafusion-functions-window-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-window-common) + Compiling datafusion-functions-aggregate-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-aggregate-common) + Compiling datafusion-expr v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/expr) + Compiling datafusion-physical-expr v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-expr) + Compiling datafusion-execution v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/execution) + Compiling datafusion-functions v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions) + Compiling datafusion-functions-aggregate v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-aggregate) + Compiling datafusion-physical-plan v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-plan) + Compiling datafusion-physical-expr-adapter v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-expr-adapter) + Compiling datafusion-functions-nested v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-nested) + Compiling datafusion-session v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/session) + Compiling datafusion-datasource v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/datasource) + Compiling datafusion-catalog v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/catalog) + Compiling datafusion-pruning v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/pruning) + Compiling datafusion-physical-optimizer v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-optimizer) + Compiling datafusion-datasource-parquet v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/datasource-parquet) + Compiling datafusion-datasource-arrow v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/datasource-arrow) + Compiling datafusion-datasource-csv v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/datasource-csv) + Compiling datafusion-datasource-json v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/datasource-json) + Compiling datafusion-datasource-avro v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/datasource-avro) + Compiling datafusion-sql v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/sql) + Compiling datafusion-optimizer v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/optimizer) + Compiling datafusion-functions-window v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-window) + Compiling datafusion-functions-table v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-table) + Compiling datafusion-catalog-listing v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/catalog-listing) + Compiling datafusion v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/core) + Compiling datafusion-spark v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/spark) + Compiling datafusion-sqllogictest v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/sqllogictest) + Finished `test` profile [unoptimized + debuginfo] target(s) in 5m 57s + Running bin/sqllogictests.rs (target/debug/deps/sqllogictests-99671dc23c43b93d) +Running with 4 test threads (available parallelism: 4) +Progress: 1/1 files completed (100%) +External error: Other Error: SLT file dictionary_group_by_key_promotion.slt left modified configuration + datafusion.execution.batch_size: 8192 -> 128 + datafusion.execution.target_partitions: 4 -> 1 +Error: Execution("1 failures") +error: test failed, to rerun pass `-p datafusion-sqllogictest --test sqllogictests` + +Caused by: + process didn't exit successfully: `/home/runner/work/datafusion/datafusion/target/debug/deps/sqllogictests-99671dc23c43b93d dictionary_group_by_key_promotion` (exit status: 1) From fd1a076a519f6396432e848c118719f077b42c10 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:34:19 -0400 Subject: [PATCH 34/41] ci: persist clippy diagnostics --- .github/workflows/validate-df23127.yml | 32 +++++++++----------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml index c00bfcec857e8..c0cb8c02bd059 100644 --- a/.github/workflows/validate-df23127.yml +++ b/.github/workflows/validate-df23127.yml @@ -9,41 +9,31 @@ permissions: contents: write jobs: - targeted-tests: + clippy-diagnostic: runs-on: ubuntu-latest timeout-minutes: 60 steps: - uses: actions/checkout@v4 with: ref: validate/df-23127-20260710 - - name: Show toolchain - run: rustc --version && cargo --version - name: Check formatting run: cargo fmt --check - - name: Test partial and final dictionary transport - run: cargo test -p datafusion-physical-plan dictionary_groups_keep_partial_schema_and_promote_final_output -- --nocapture - - name: Test monotonic dictionary promotion - run: cargo test -p datafusion-physical-plan dict_uint8_utf8_promotes_and_does_not_shrink -- --nocapture - - name: Test legacy single-stage promotion - run: cargo test -p datafusion-physical-plan dictionary_group_key_promotes_runtime_schema -- --nocapture - - name: Test SQL dictionary promotion - id: sql + - name: Run clippy + id: clippy continue-on-error: true run: | set -o pipefail - cargo test --test sqllogictests -- dictionary_group_by_key_promotion 2>&1 | tee /tmp/df23127-sql.log - - name: Persist SQL failure log - if: steps.sql.outcome == 'failure' + cargo clippy -p datafusion-physical-plan --all-targets -- -D warnings 2>&1 | tee /tmp/df23127-clippy.log + - name: Persist clippy failure log + if: steps.clippy.outcome == 'failure' run: | mkdir -p validation - cp /tmp/df23127-sql.log validation/df23127-sql.log + cp /tmp/df23127-clippy.log validation/df23127-clippy.log git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add validation/df23127-sql.log - git commit -m "ci: capture df23127 SQL failure [skip ci]" + git add validation/df23127-clippy.log + git commit -m "ci: capture df23127 clippy failure [skip ci]" git push origin HEAD:validate/df-23127-20260710 - - name: Run clippy - run: cargo clippy -p datafusion-physical-plan --all-targets -- -D warnings - - name: Fail when SQL regression fails - if: steps.sql.outcome == 'failure' + - name: Fail when clippy fails + if: steps.clippy.outcome == 'failure' run: exit 1 From fc7be7f4f8f9fcb766a526a8a87b3b9815d8102e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:36:16 +0000 Subject: [PATCH 35/41] ci: capture df23127 clippy failure [skip ci] --- validation/df23127-clippy.log | 455 ++++++++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 validation/df23127-clippy.log diff --git a/validation/df23127-clippy.log b/validation/df23127-clippy.log new file mode 100644 index 0000000000000..f1a78ff81a337 --- /dev/null +++ b/validation/df23127-clippy.log @@ -0,0 +1,455 @@ + Updating crates.io index + Downloading crates ... + Downloaded futures v0.3.32 + Downloaded pin-project-internal v1.1.13 + Downloaded const-random v0.1.18 + Downloaded toml_parser v1.1.2+spec-1.1.0 + Downloaded rand v0.8.6 + Downloaded phf v0.12.1 + Downloaded oorandom v11.1.5 + Downloaded lz4_flex v0.13.0 + Downloaded arrow-string v59.1.0 + Downloaded parking_lot v0.12.5 + Downloaded anes v0.1.6 + Downloaded proc-macro-crate v3.5.0 + Downloaded plotters-svg v0.3.7 + Downloaded potential_utf v0.1.5 + Downloaded jobserver v0.1.34 + Downloaded arrow-cast v59.1.0 + Downloaded toml_datetime v1.1.1+spec-1.1.0 + Downloaded rand_core v0.6.4 + Downloaded cast v0.3.0 + Downloaded utf8_iter v1.0.4 + Downloaded crossbeam-deque v0.8.6 + Downloaded strip-ansi-escapes v0.2.1 + Downloaded twox-hash v2.1.2 + Downloaded winnow v1.0.2 + Downloaded zmij v1.0.21 + Downloaded getrandom v0.2.17 + Downloaded icu_normalizer v2.2.0 + Downloaded writeable v0.6.3 + Downloaded rstest v0.26.1 + Downloaded zstd-safe v7.2.4 + Downloaded zerovec-derive v0.11.3 + Downloaded serde_derive v1.0.228 + Downloaded arrow-buffer v59.1.0 + Downloaded zerocopy-derive v0.8.48 + Downloaded zerofrom-derive v0.1.7 + Downloaded percent-encoding v2.3.2 + Downloaded rand_core v0.9.5 + Downloaded relative-path v1.9.3 + Downloaded version_check v0.9.5 + Downloaded thiserror-impl v2.0.18 + Downloaded tinystr v0.8.3 + Downloaded rand_chacha v0.9.0 + Downloaded siphasher v1.0.2 + Downloaded yoke-derive v0.8.2 + Downloaded zerofrom v0.1.7 + Downloaded ppv-lite86 v0.2.21 + Downloaded yoke v0.8.2 + Downloaded tempfile v3.27.0 + Downloaded tinytemplate v1.2.1 + Downloaded simdutf8 v0.1.5 + Downloaded shlex v1.3.0 + Downloaded semver v1.0.28 + Downloaded ryu v1.0.23 + Downloaded zerotrie v0.2.4 + Downloaded num-traits v0.2.19 + Downloaded chrono-tz v0.10.4 + Downloaded unicode-ident v1.0.24 + Downloaded similar v2.7.0 + Downloaded tracing-core v0.1.36 + Downloaded log v0.4.33 + Downloaded vte v0.14.1 + Downloaded rand v0.9.4 + Downloaded toml_edit v0.25.11+spec-1.1.0 + Downloaded num-bigint v0.4.6 + Downloaded proc-macro2 v1.0.106 + Downloaded rayon-core v1.13.0 + Downloaded unicode-segmentation v1.13.2 + Downloaded uuid v1.23.4 + Downloaded lexical-write-float v1.0.6 + Downloaded lexical-util v1.0.7 + Downloaded rstest_macros v0.26.1 + Downloaded rayon v1.12.0 + Downloaded litemap v0.8.2 + Downloaded num-integer v0.1.46 + Downloaded bstr v1.12.1 + Downloaded zerovec v0.11.6 + Downloaded plotters v0.3.7 + Downloaded tokio-util v0.7.18 + Downloaded serde_json v1.0.150 + Downloaded csv v1.4.0 + Downloaded regex v1.12.4 + Downloaded libc v0.2.186 + Downloaded lexical-parse-float v1.0.6 + Downloaded libm v0.2.16 + Downloaded smallvec v1.15.1 + Downloaded slab v0.4.12 + Downloaded rand_chacha v0.3.1 + Downloaded pin-project v1.1.13 + Downloaded unicode-width v0.2.2 + Downloaded rstest_reuse v0.7.0 + Downloaded lexical-parse-integer v1.0.6 + Downloaded url v2.5.8 + Downloaded tokio-macros v2.7.0 + Downloaded synstructure v0.13.2 + Downloaded serde v1.0.228 + Downloaded lexical-write-integer v1.0.6 + Downloaded zerocopy v0.8.48 + Downloaded syn v2.0.118 + Downloaded regex-syntax v0.8.11 + Downloaded object_store v0.13.2 + Downloaded clap_builder v4.6.0 + Downloaded chrono v0.4.45 + Downloaded plotters-backend v0.3.7 + Downloaded pkg-config v0.3.33 + Downloaded tracing v0.1.44 + Downloaded rustix v1.1.4 + Downloaded quote v1.0.46 + Downloaded arrow-ipc v59.1.0 + Downloaded arrow-select v59.1.0 + Downloaded insta v1.48.0 + Downloaded http v1.4.0 + Downloaded stable_deref_trait v1.2.1 + Downloaded scopeguard v1.2.0 + Downloaded indexmap v2.14.0 + Downloaded idna v1.1.0 + Downloaded icu_normalizer_data v2.2.0 + Downloaded serde_core v1.0.228 + Downloaded petgraph v0.8.3 + Downloaded regex-automata v0.4.14 + Downloaded page_size v0.6.0 + Downloaded half v2.7.1 + Downloaded arrow-csv v59.1.0 + Downloaded zstd-sys v2.0.16+zstd.1.5.7 + Downloaded arrow v59.1.0 + Downloaded phf_shared v0.12.1 + Downloaded lexical-core v1.0.6 + Downloaded itertools v0.14.0 + Downloaded icu_provider v2.2.0 + Downloaded globset v0.4.18 + Downloaded getrandom v0.3.4 + Downloaded futures-util v0.3.32 + Downloaded crossbeam-utils v0.8.21 + Downloaded criterion-plot v0.8.2 + Downloaded console v0.16.3 + Downloaded cfg-if v1.0.4 + Downloaded anstyle v1.0.14 + Downloaded tokio v1.52.3 + Downloaded zstd v0.13.3 + Downloaded itoa v1.0.18 + Downloaded hashbrown v0.14.5 + Downloaded futures-core v0.3.32 + Downloaded equivalent v1.0.2 + Downloaded dashmap v6.2.1 + Downloaded csv-core v0.1.13 + Downloaded autocfg v1.5.0 + Downloaded atoi v2.0.0 + Downloaded async-trait v0.1.89 + Downloaded idna_adapter v1.2.1 + Downloaded errno v0.3.14 + Downloaded displaydoc v0.2.5 + Downloaded ciborium-io v0.2.2 + Downloaded walkdir v2.5.0 + Downloaded tracing-attributes v0.1.31 + Downloaded parking_lot_core v0.9.12 + Downloaded once_cell v1.21.4 + Downloaded num-complex v0.4.6 + Downloaded glob v0.3.3 + Downloaded futures-task v0.3.32 + Downloaded futures-executor v0.3.32 + Downloaded futures-channel v0.3.32 + Downloaded either v1.15.0 + Downloaded comfy-table v7.2.2 + Downloaded bitflags v2.11.1 + Downloaded base64 v0.22.1 + Downloaded arrow-ord v59.1.0 + Downloaded alloca v0.4.0 + Downloaded icu_locale_core v2.2.0 + Downloaded getrandom v0.4.2 + Downloaded futures-sink v0.3.32 + Downloaded futures-io v0.3.32 + Downloaded ciborium-ll v0.2.2 + Downloaded crossbeam-epoch v0.9.20 + Downloaded arrow-row v59.1.0 + Downloaded arrow-data v59.1.0 + Downloaded arrow-array v59.1.0 + Downloaded allocator-api2 v0.2.21 + Downloaded iana-time-zone v0.1.65 + Downloaded futures-timer v3.0.3 + Downloaded flatbuffers v25.12.19 + Downloaded criterion v0.8.2 + Downloaded const-random-macro v0.1.16 + Downloaded clap_lex v1.1.0 + Downloaded ciborium v0.2.2 + Downloaded itertools v0.15.0 + Downloaded itertools v0.13.0 + Downloaded icu_properties_data v2.2.0 + Downloaded icu_properties v2.2.0 + Downloaded icu_collections v2.2.0 + Downloaded humantime v2.3.0 + Downloaded hex v0.4.3 + Downloaded hashbrown v0.17.1 + Downloaded hashbrown v0.15.5 + Downloaded form_urlencoded v1.2.2 + Downloaded foldhash v0.1.5 + Downloaded fixedbitset v0.5.7 + Downloaded fastrand v2.4.1 + Downloaded cc v1.2.61 + Downloaded bytes v1.12.0 + Downloaded tiny-keccak v2.0.2 + Downloaded thiserror v2.0.18 + Downloaded same-file v1.0.6 + Downloaded rustc_version v0.4.1 + Downloaded foldhash v0.2.0 + Downloaded clap v4.6.1 + Downloaded find-msvc-tools v0.1.9 + Downloaded futures-macro v0.3.32 + Downloaded crunchy v0.2.4 + Downloaded arrow-json v59.1.0 + Downloaded ahash v0.8.12 + Downloaded memchr v2.8.2 + Downloaded aho-corasick v1.1.4 + Downloaded pin-project-lite v0.2.17 + Downloaded arrow-schema v59.1.0 + Downloaded arrow-arith v59.1.0 + Downloaded lock_api v0.4.14 + Downloaded linux-raw-sys v0.12.1 + Compiling proc-macro2 v1.0.106 + Compiling quote v1.0.46 + Compiling unicode-ident v1.0.24 + Compiling libc v0.2.186 + Checking cfg-if v1.0.4 + Checking memchr v2.8.2 + Compiling autocfg v1.5.0 + Compiling libm v0.2.16 + Compiling num-traits v0.2.19 + Compiling syn v2.0.118 + Compiling zerocopy v0.8.48 + Compiling serde_core v1.0.228 + Checking equivalent v1.0.2 + Checking foldhash v0.2.0 + Checking itoa v1.0.18 + Checking allocator-api2 v0.2.21 + Checking bytes v1.12.0 + Checking hashbrown v0.17.1 + Compiling zmij v1.0.21 + Checking once_cell v1.21.4 + Compiling getrandom v0.3.4 + Checking indexmap v2.14.0 + Compiling serde_json v1.0.150 + Checking num-integer v0.1.46 + Checking siphasher v1.0.2 + Checking iana-time-zone v0.1.65 + Compiling version_check v0.9.5 + Checking num-bigint v0.4.6 + Compiling ahash v0.8.12 + Checking chrono v0.4.45 + Checking phf_shared v0.12.1 + Compiling jobserver v0.1.34 + Compiling chrono-tz v0.10.4 + Compiling find-msvc-tools v0.1.9 + Compiling shlex v1.3.0 + Compiling cc v1.2.61 + Checking phf v0.12.1 + Checking arrow-schema v59.1.0 + Checking num-complex v0.4.6 + Checking smallvec v1.15.1 + Checking pin-project-lite v0.2.17 + Compiling semver v1.0.28 + Compiling rustc_version v0.4.1 + Compiling zerocopy-derive v0.8.48 + Compiling synstructure v0.13.2 + Checking lexical-util v1.0.7 + Checking aho-corasick v1.1.4 + Compiling parking_lot_core v0.9.12 + Compiling pkg-config v0.3.33 + Checking regex-syntax v0.8.11 + Compiling zstd-sys v2.0.16+zstd.1.5.7 + Compiling zerofrom-derive v0.1.7 + Checking either v1.15.0 + Checking regex-automata v0.4.14 + Checking scopeguard v1.2.0 + Checking lock_api v0.4.14 + Checking zerofrom v0.1.7 + Compiling yoke-derive v0.8.2 + Checking lexical-write-integer v1.0.6 + Checking lexical-parse-integer v1.0.6 + Checking bitflags v2.11.1 + Checking stable_deref_trait v1.2.1 + Checking lexical-parse-float v1.0.6 + Checking yoke v0.8.2 + Checking lexical-write-float v1.0.6 + Checking regex v1.12.4 + Checking half v2.7.1 + Checking unicode-segmentation v1.13.2 + Checking ryu v1.0.23 + Checking arrow-buffer v59.1.0 + Compiling zstd-safe v7.2.4 + Checking unicode-width v0.2.2 + Compiling getrandom v0.4.2 + Checking comfy-table v7.2.2 + Checking lexical-core v1.0.6 + Checking parking_lot v0.12.5 + Compiling flatbuffers v25.12.19 + Checking arrow-data v59.1.0 + Compiling tokio-macros v2.7.0 + Compiling zerovec-derive v0.11.3 + Checking arrow-array v59.1.0 + Checking atoi v2.0.0 + Checking base64 v0.22.1 + Checking tokio v1.52.3 + Checking zerovec v0.11.6 + Compiling displaydoc v0.2.5 + Checking csv-core v0.1.13 + Checking log v0.4.33 + Checking twox-hash v2.1.2 + Checking lz4_flex v0.13.0 + Checking arrow-select v59.1.0 + Checking csv v1.4.0 + Checking simdutf8 v0.1.5 + Checking arrow-row v59.1.0 + Checking arrow-ord v59.1.0 + Checking arrow-string v59.1.0 + Checking arrow-cast v59.1.0 + Checking arrow-arith v59.1.0 + Checking futures-core v0.3.32 + Checking futures-sink v0.3.32 + Checking tinystr v0.8.3 + Checking uuid v1.23.4 + Checking itertools v0.15.0 + Checking litemap v0.8.2 + Checking writeable v0.6.3 + Compiling crossbeam-utils v0.8.21 + Checking arrow-csv v59.1.0 + Checking arrow-json v59.1.0 + Checking icu_locale_core v2.2.0 + Checking futures-channel v0.3.32 + Checking zerotrie v0.2.4 + Checking potential_utf v0.1.5 + Compiling pin-project-internal v1.1.13 + Compiling futures-macro v0.3.32 + Compiling icu_normalizer_data v2.2.0 + Checking futures-io v0.3.32 + Checking slab v0.4.12 + Checking futures-task v0.3.32 + Checking utf8_iter v1.0.4 + Compiling icu_properties_data v2.2.0 + Checking icu_collections v2.2.0 + Checking futures-util v0.3.32 + Checking icu_provider v2.2.0 + Checking pin-project v1.1.13 + Checking icu_properties v2.2.0 + Checking icu_normalizer v2.2.0 + Compiling async-trait v0.1.89 + Compiling rustix v1.1.4 + Checking same-file v1.0.6 + Checking walkdir v2.5.0 + Checking idna_adapter v1.2.1 + Checking percent-encoding v2.3.2 + Checking datafusion-doc v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/doc) + Checking linux-raw-sys v0.12.1 + Compiling thiserror v2.0.18 + Checking form_urlencoded v1.2.2 + Checking futures-executor v0.3.32 + Checking idna v1.1.0 + Checking futures v0.3.32 + Checking ppv-lite86 v0.2.21 + Compiling thiserror-impl v2.0.18 + Compiling tracing-attributes v0.1.31 + Checking rand_core v0.9.5 + Checking tracing-core v0.1.36 + Compiling crossbeam-epoch v0.9.20 + Checking fastrand v2.4.1 + Checking foldhash v0.1.5 + Checking tracing v0.1.44 + Checking hashbrown v0.15.5 + Checking tempfile v3.27.0 + Checking rand_chacha v0.9.0 + Checking url v2.5.8 + Checking itertools v0.14.0 + Checking http v1.4.0 + Checking humantime v2.3.0 + Compiling winnow v1.0.2 + Checking hashbrown v0.14.5 + Compiling serde v1.0.228 + Checking fixedbitset v0.5.7 + Checking dashmap v6.2.1 + Checking petgraph v0.8.3 + Compiling toml_parser v1.1.2+spec-1.1.0 + Compiling datafusion-macros v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/macros) + Checking object_store v0.13.2 + Checking rand v0.9.4 + Compiling getrandom v0.2.17 + Checking tokio-util v0.7.18 + Compiling serde_derive v1.0.228 + Compiling rayon-core v1.13.0 + Compiling toml_datetime v1.1.1+spec-1.1.0 + Compiling toml_edit v0.25.11+spec-1.1.0 + Compiling rand_core v0.6.4 + Checking crossbeam-deque v0.8.6 + Compiling rstest_macros v0.26.1 + Compiling alloca v0.4.0 + Checking plotters-backend v0.3.7 + Checking ciborium-io v0.2.2 + Checking clap_lex v1.1.0 + Checking anstyle v1.0.14 + Checking ciborium-ll v0.2.2 + Checking plotters-svg v0.3.7 + Checking clap_builder v4.6.0 + Compiling rand_chacha v0.3.1 + Compiling proc-macro-crate v3.5.0 + Checking itertools v0.13.0 + Checking bstr v1.12.1 + Checking vte v0.14.1 + Checking cast v0.3.0 + Compiling glob v0.3.3 + Compiling relative-path v1.9.3 + Checking hex v0.4.3 + Checking criterion-plot v0.8.2 + Checking strip-ansi-escapes v0.2.1 + Checking globset v0.4.18 + Compiling rand v0.8.6 + Checking rayon v1.12.0 + Checking clap v4.6.1 + Checking plotters v0.3.7 + Checking zstd v0.13.3 + Checking arrow-ipc v59.1.0 + Checking ciborium v0.2.2 + Checking tinytemplate v1.2.1 + Checking arrow v59.1.0 + Checking datafusion-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/common) + Checking datafusion-common-runtime v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/common-runtime) + Checking page_size v0.6.0 + Checking console v0.16.3 + Checking anes v0.1.6 + Checking futures-timer v3.0.3 + Checking similar v2.7.0 + Checking oorandom v11.1.5 + Checking criterion v0.8.2 + Checking insta v1.48.0 + Checking rstest v0.26.1 + Compiling rstest_reuse v0.7.0 + Checking datafusion-expr-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/expr-common) + Checking datafusion-physical-expr-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-expr-common) + Checking datafusion-functions-window-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-window-common) + Checking datafusion-functions-aggregate-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-aggregate-common) + Checking datafusion-expr v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/expr) + Checking datafusion-execution v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/execution) + Checking datafusion-physical-expr v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-expr) + Checking datafusion-functions v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions) + Checking datafusion-functions-aggregate v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-aggregate) + Checking datafusion-functions-window v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-window) + Checking datafusion-physical-plan v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-plan) +error: this call to `clone` can be replaced with `std::slice::from_ref` + --> datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs:51:9 + | +51 | &[input_batches.clone()], + | ^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `std::slice::from_ref(&input_batches)` + | + = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.96.0/index.html#cloned_ref_to_slice_refs + = note: `-D clippy::cloned-ref-to-slice-refs` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::cloned_ref_to_slice_refs)]` + +error: could not compile `datafusion-physical-plan` (lib test) due to 1 previous error From 9f931f4a90b08b3254e7f373ebad3ea3102e535a Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:37:33 -0400 Subject: [PATCH 36/41] ci: validate final df23127 source patch --- .github/workflows/validate-df23127.yml | 139 ++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 12 deletions(-) diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml index c0cb8c02bd059..8afdecafc4804 100644 --- a/.github/workflows/validate-df23127.yml +++ b/.github/workflows/validate-df23127.yml @@ -8,32 +8,147 @@ on: permissions: contents: write +env: + CARGO_INCREMENTAL: 0 + jobs: - clippy-diagnostic: + targeted-validation: runs-on: ubuntu-latest timeout-minutes: 60 steps: - uses: actions/checkout@v4 with: ref: validate/df-23127-20260710 + - name: Apply source patch to validation and PR branches + run: | + cat > /tmp/apply_df23127_patch.py <<'PY' + from pathlib import Path + + def replace_once(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text() + if new in text: + return + count = text.count(old) + if count != 1: + raise RuntimeError(f"expected one match in {path}, found {count}") + file.write_text(text.replace(old, new, 1)) + + replace_once( + "datafusion/physical-plan/src/aggregates/hash_stream.rs", + """ Ok(Some(batch)) => { + let _ = self + .reservation + .try_resize(original_state.hash_table().memory_size()); + debug_assert!(batch.num_rows() > 0); + let next_state = if original_state.hash_table().is_done() { + original_state.into_done()""", + """ Ok(Some(batch)) => { + self.schema = batch.schema(); + let _ = self + .reservation + .try_resize(original_state.hash_table().memory_size()); + debug_assert!(batch.num_rows() > 0); + let next_state = if original_state.hash_table().is_done() { + original_state.into_done()""", + ) + replace_once( + "datafusion/physical-plan/src/aggregates/ordered_final_stream.rs", + """ Ok(Some(batch)) => { + let next_state = + OrderedFinalAggregateState::ReadingInput { table };""", + """ Ok(Some(batch)) => { + self.schema = batch.schema(); + let next_state = + OrderedFinalAggregateState::ReadingInput { table };""", + ) + replace_once( + "datafusion/physical-plan/src/aggregates/ordered_final_stream.rs", + """ Ok(Some(batch)) => { + let next_state = if table.is_empty() {""", + """ Ok(Some(batch)) => { + self.schema = batch.schema(); + let next_state = if table.is_empty() {""", + ) + + sql_file = Path( + "datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt" + ) + sql = sql_file.read_text() + reset = """ + + statement ok + SET datafusion.execution.batch_size = 8192; + + statement ok + SET datafusion.execution.target_partitions = 4; + """ + if reset.strip() not in sql: + sql_file.write_text(sql.rstrip() + reset) + PY + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + patch_branch() { + remote_branch="$1" + local_branch="$2" + message="$3" + git fetch origin "$remote_branch" + git switch --force-create "$local_branch" "origin/$remote_branch" + python /tmp/apply_df23127_patch.py + cargo fmt + if ! git diff --quiet; then + git add \ + datafusion/physical-plan/src/aggregates/hash_stream.rs \ + datafusion/physical-plan/src/aggregates/ordered_final_stream.rs \ + datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt + git commit -m "$message" + git push origin HEAD:"$remote_branch" + fi + } + + patch_branch \ + fix/df-23127-dict-group-values \ + work-pr \ + "fix: keep promoted aggregate stream schemas current" + patch_branch \ + validate/df-23127-20260710 \ + work-validation \ + "ci: mirror final df23127 source patch [skip ci]" - name: Check formatting run: cargo fmt --check - - name: Run clippy + - name: Test partial and final dictionary transport + run: cargo test -p datafusion-physical-plan dictionary_groups_keep_partial_schema_and_promote_final_output -- --nocapture + - name: Test monotonic dictionary promotion + run: cargo test -p datafusion-physical-plan dict_uint8_utf8_promotes_and_does_not_shrink -- --nocapture + - name: Test legacy single-stage promotion + run: cargo test -p datafusion-physical-plan dictionary_group_key_promotes_runtime_schema -- --nocapture + - name: Test SQL dictionary promotion + id: sql + continue-on-error: true + run: | + set -o pipefail + cargo test --test sqllogictests -- dictionary_group_by_key_promotion 2>&1 | tee /tmp/df23127-sql.log + - name: Run clippy for library and tests id: clippy continue-on-error: true run: | set -o pipefail - cargo clippy -p datafusion-physical-plan --all-targets -- -D warnings 2>&1 | tee /tmp/df23127-clippy.log - - name: Persist clippy failure log - if: steps.clippy.outcome == 'failure' + cargo clippy -p datafusion-physical-plan --lib --tests -- -D warnings 2>&1 | tee /tmp/df23127-clippy.log + - name: Persist validation failures + if: steps.sql.outcome == 'failure' || steps.clippy.outcome == 'failure' run: | mkdir -p validation - cp /tmp/df23127-clippy.log validation/df23127-clippy.log - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add validation/df23127-clippy.log - git commit -m "ci: capture df23127 clippy failure [skip ci]" + if [ "${{ steps.sql.outcome }}" = "failure" ]; then + cp /tmp/df23127-sql.log validation/df23127-sql.log + fi + if [ "${{ steps.clippy.outcome }}" = "failure" ]; then + cp /tmp/df23127-clippy.log validation/df23127-clippy.log + fi + git add validation + git commit -m "ci: capture final df23127 validation failure [skip ci]" git push origin HEAD:validate/df-23127-20260710 - - name: Fail when clippy fails - if: steps.clippy.outcome == 'failure' + - name: Fail on validation error + if: steps.sql.outcome == 'failure' || steps.clippy.outcome == 'failure' run: exit 1 From a3882b2ae4c2393d0591bb1ae3060e59c3c68b2c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:39:36 +0000 Subject: [PATCH 37/41] ci: mirror final df23127 source patch [skip ci] --- datafusion/physical-plan/src/aggregates/hash_stream.rs | 1 + .../physical-plan/src/aggregates/ordered_final_stream.rs | 2 ++ .../test_files/dictionary_group_by_key_promotion.slt | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/datafusion/physical-plan/src/aggregates/hash_stream.rs b/datafusion/physical-plan/src/aggregates/hash_stream.rs index 62b92965030ae..323f90022175f 100644 --- a/datafusion/physical-plan/src/aggregates/hash_stream.rs +++ b/datafusion/physical-plan/src/aggregates/hash_stream.rs @@ -899,6 +899,7 @@ impl FinalHashAggregateStream { match result { Ok(Some(batch)) => { + self.schema = batch.schema(); let _ = self .reservation .try_resize(original_state.hash_table().memory_size()); diff --git a/datafusion/physical-plan/src/aggregates/ordered_final_stream.rs b/datafusion/physical-plan/src/aggregates/ordered_final_stream.rs index 89653e05ab4c7..e7ae8e30568d3 100644 --- a/datafusion/physical-plan/src/aggregates/ordered_final_stream.rs +++ b/datafusion/physical-plan/src/aggregates/ordered_final_stream.rs @@ -169,6 +169,7 @@ impl OrderedFinalAggregateStream { // Some finalized groups can be emitted. Yield them, then // continue aggregating input in the current state. Ok(Some(batch)) => { + self.schema = batch.schema(); let next_state = OrderedFinalAggregateState::ReadingInput { table }; self.resize_reservation_for_state(&next_state); @@ -237,6 +238,7 @@ impl OrderedFinalAggregateStream { match result { Ok(Some(batch)) => { + self.schema = batch.schema(); let next_state = if table.is_empty() { OrderedFinalAggregateState::Done } else { diff --git a/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt b/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt index 04e080d2e7d3d..ae7b6b577828c 100644 --- a/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt +++ b/datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt @@ -67,3 +67,9 @@ FROM ( ); ---- Dictionary(UInt16, Utf8) + +statement ok +SET datafusion.execution.batch_size = 8192; + +statement ok +SET datafusion.execution.target_partitions = 4; From 1d5b35ff0a63b817a602f8ec5bdddb25f8903593 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:43:52 -0400 Subject: [PATCH 38/41] ci: run final read-only df23127 validation --- .github/workflows/validate-df23127.yml | 130 +------------------------ 1 file changed, 5 insertions(+), 125 deletions(-) diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml index 8afdecafc4804..572cb61debd67 100644 --- a/.github/workflows/validate-df23127.yml +++ b/.github/workflows/validate-df23127.yml @@ -6,116 +6,20 @@ on: - main permissions: - contents: write + contents: read env: CARGO_INCREMENTAL: 0 + CARGO_BUILD_JOBS: 2 jobs: targeted-validation: runs-on: ubuntu-latest - timeout-minutes: 60 + timeout-minutes: 90 steps: - uses: actions/checkout@v4 with: ref: validate/df-23127-20260710 - - name: Apply source patch to validation and PR branches - run: | - cat > /tmp/apply_df23127_patch.py <<'PY' - from pathlib import Path - - def replace_once(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text() - if new in text: - return - count = text.count(old) - if count != 1: - raise RuntimeError(f"expected one match in {path}, found {count}") - file.write_text(text.replace(old, new, 1)) - - replace_once( - "datafusion/physical-plan/src/aggregates/hash_stream.rs", - """ Ok(Some(batch)) => { - let _ = self - .reservation - .try_resize(original_state.hash_table().memory_size()); - debug_assert!(batch.num_rows() > 0); - let next_state = if original_state.hash_table().is_done() { - original_state.into_done()""", - """ Ok(Some(batch)) => { - self.schema = batch.schema(); - let _ = self - .reservation - .try_resize(original_state.hash_table().memory_size()); - debug_assert!(batch.num_rows() > 0); - let next_state = if original_state.hash_table().is_done() { - original_state.into_done()""", - ) - replace_once( - "datafusion/physical-plan/src/aggregates/ordered_final_stream.rs", - """ Ok(Some(batch)) => { - let next_state = - OrderedFinalAggregateState::ReadingInput { table };""", - """ Ok(Some(batch)) => { - self.schema = batch.schema(); - let next_state = - OrderedFinalAggregateState::ReadingInput { table };""", - ) - replace_once( - "datafusion/physical-plan/src/aggregates/ordered_final_stream.rs", - """ Ok(Some(batch)) => { - let next_state = if table.is_empty() {""", - """ Ok(Some(batch)) => { - self.schema = batch.schema(); - let next_state = if table.is_empty() {""", - ) - - sql_file = Path( - "datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt" - ) - sql = sql_file.read_text() - reset = """ - - statement ok - SET datafusion.execution.batch_size = 8192; - - statement ok - SET datafusion.execution.target_partitions = 4; - """ - if reset.strip() not in sql: - sql_file.write_text(sql.rstrip() + reset) - PY - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - patch_branch() { - remote_branch="$1" - local_branch="$2" - message="$3" - git fetch origin "$remote_branch" - git switch --force-create "$local_branch" "origin/$remote_branch" - python /tmp/apply_df23127_patch.py - cargo fmt - if ! git diff --quiet; then - git add \ - datafusion/physical-plan/src/aggregates/hash_stream.rs \ - datafusion/physical-plan/src/aggregates/ordered_final_stream.rs \ - datafusion/sqllogictest/test_files/dictionary_group_by_key_promotion.slt - git commit -m "$message" - git push origin HEAD:"$remote_branch" - fi - } - - patch_branch \ - fix/df-23127-dict-group-values \ - work-pr \ - "fix: keep promoted aggregate stream schemas current" - patch_branch \ - validate/df-23127-20260710 \ - work-validation \ - "ci: mirror final df23127 source patch [skip ci]" - name: Check formatting run: cargo fmt --check - name: Test partial and final dictionary transport @@ -125,30 +29,6 @@ jobs: - name: Test legacy single-stage promotion run: cargo test -p datafusion-physical-plan dictionary_group_key_promotes_runtime_schema -- --nocapture - name: Test SQL dictionary promotion - id: sql - continue-on-error: true - run: | - set -o pipefail - cargo test --test sqllogictests -- dictionary_group_by_key_promotion 2>&1 | tee /tmp/df23127-sql.log + run: cargo test --test sqllogictests -- dictionary_group_by_key_promotion - name: Run clippy for library and tests - id: clippy - continue-on-error: true - run: | - set -o pipefail - cargo clippy -p datafusion-physical-plan --lib --tests -- -D warnings 2>&1 | tee /tmp/df23127-clippy.log - - name: Persist validation failures - if: steps.sql.outcome == 'failure' || steps.clippy.outcome == 'failure' - run: | - mkdir -p validation - if [ "${{ steps.sql.outcome }}" = "failure" ]; then - cp /tmp/df23127-sql.log validation/df23127-sql.log - fi - if [ "${{ steps.clippy.outcome }}" = "failure" ]; then - cp /tmp/df23127-clippy.log validation/df23127-clippy.log - fi - git add validation - git commit -m "ci: capture final df23127 validation failure [skip ci]" - git push origin HEAD:validate/df-23127-20260710 - - name: Fail on validation error - if: steps.sql.outcome == 'failure' || steps.clippy.outcome == 'failure' - run: exit 1 + run: cargo clippy -p datafusion-physical-plan --lib --tests -- -D warnings From 9de8d99bd6eb2a9bb7f50b61f1ecd5502caadbda Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:53:26 -0400 Subject: [PATCH 39/41] ci: capture final clippy diagnostic --- .github/workflows/validate-df23127.yml | 33 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/validate-df23127.yml b/.github/workflows/validate-df23127.yml index 572cb61debd67..12578aa5a0da6 100644 --- a/.github/workflows/validate-df23127.yml +++ b/.github/workflows/validate-df23127.yml @@ -6,14 +6,14 @@ on: - main permissions: - contents: read + contents: write env: CARGO_INCREMENTAL: 0 CARGO_BUILD_JOBS: 2 jobs: - targeted-validation: + clippy-diagnostic: runs-on: ubuntu-latest timeout-minutes: 90 steps: @@ -22,13 +22,24 @@ jobs: ref: validate/df-23127-20260710 - name: Check formatting run: cargo fmt --check - - name: Test partial and final dictionary transport - run: cargo test -p datafusion-physical-plan dictionary_groups_keep_partial_schema_and_promote_final_output -- --nocapture - - name: Test monotonic dictionary promotion - run: cargo test -p datafusion-physical-plan dict_uint8_utf8_promotes_and_does_not_shrink -- --nocapture - - name: Test legacy single-stage promotion - run: cargo test -p datafusion-physical-plan dictionary_group_key_promotes_runtime_schema -- --nocapture - - name: Test SQL dictionary promotion - run: cargo test --test sqllogictests -- dictionary_group_by_key_promotion - name: Run clippy for library and tests - run: cargo clippy -p datafusion-physical-plan --lib --tests -- -D warnings + id: clippy + continue-on-error: true + run: | + set -o pipefail + cargo clippy -p datafusion-physical-plan --lib --tests -- -D warnings 2>&1 | tee /tmp/df23127-clippy.log + - name: Persist clippy diagnostic tail + if: steps.clippy.outcome == 'failure' + run: | + mkdir -p validation + tail -n 200 /tmp/df23127-clippy.log > validation/df23127-clippy-tail.log + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add validation/df23127-clippy-tail.log + if ! git diff --cached --quiet; then + git commit -m "ci: capture final df23127 clippy diagnostic [skip ci]" + git push origin HEAD:validate/df-23127-20260710 + fi + - name: Fail when clippy fails + if: steps.clippy.outcome == 'failure' + run: exit 1 From 097feb50128a54112c41df8da1524ef3c5e20e07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:57:48 +0000 Subject: [PATCH 40/41] ci: capture final df23127 clippy diagnostic [skip ci] --- validation/df23127-clippy-tail.log | 200 +++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 validation/df23127-clippy-tail.log diff --git a/validation/df23127-clippy-tail.log b/validation/df23127-clippy-tail.log new file mode 100644 index 0000000000000..d5290ecb29473 --- /dev/null +++ b/validation/df23127-clippy-tail.log @@ -0,0 +1,200 @@ + Compiling cc v1.2.61 + Checking arrow-buffer v59.1.0 + Checking phf v0.12.1 + Checking arrow-schema v59.1.0 + Checking arrow-data v59.1.0 + Checking num-complex v0.4.6 + Checking smallvec v1.15.1 + Checking pin-project-lite v0.2.17 + Compiling semver v1.0.28 + Compiling rustc_version v0.4.1 + Compiling synstructure v0.13.2 + Checking lexical-util v1.0.7 + Checking aho-corasick v1.1.4 + Compiling pkg-config v0.3.33 + Compiling parking_lot_core v0.9.12 + Checking regex-syntax v0.8.11 + Checking arrow-array v59.1.0 + Checking regex-automata v0.4.14 + Checking arrow-select v59.1.0 + Compiling zstd-sys v2.0.16+zstd.1.5.7 + Compiling zerofrom-derive v0.1.7 + Checking either v1.15.0 + Checking scopeguard v1.2.0 + Checking lock_api v0.4.14 + Checking zerofrom v0.1.7 + Checking lexical-parse-integer v1.0.6 + Checking lexical-write-integer v1.0.6 + Compiling yoke-derive v0.8.2 + Checking bitflags v2.11.1 + Checking stable_deref_trait v1.2.1 + Checking yoke v0.8.2 + Checking lexical-write-float v1.0.6 + Checking lexical-parse-float v1.0.6 + Checking regex v1.12.4 + Checking ryu v1.0.23 + Compiling getrandom v0.4.2 + Compiling zstd-safe v7.2.4 + Checking unicode-segmentation v1.13.2 + Checking unicode-width v0.2.2 + Checking comfy-table v7.2.2 + Checking lexical-core v1.0.6 + Checking parking_lot v0.12.5 + Checking arrow-ord v59.1.0 + Compiling flatbuffers v25.12.19 + Compiling tokio-macros v2.7.0 + Compiling zerovec-derive v0.11.3 + Checking atoi v2.0.0 + Checking base64 v0.22.1 + Checking arrow-cast v59.1.0 + Checking zerovec v0.11.6 + Checking tokio v1.52.3 + Compiling displaydoc v0.2.5 + Checking csv-core v0.1.13 + Checking twox-hash v2.1.2 + Checking log v0.4.33 + Checking lz4_flex v0.13.0 + Checking csv v1.4.0 + Checking simdutf8 v0.1.5 + Checking arrow-json v59.1.0 + Checking arrow-csv v59.1.0 + Checking arrow-string v59.1.0 + Checking arrow-arith v59.1.0 + Checking arrow-row v59.1.0 + Checking futures-core v0.3.32 + Checking futures-sink v0.3.32 + Checking tinystr v0.8.3 + Checking uuid v1.23.4 + Checking itertools v0.15.0 + Checking zstd v0.13.3 + Checking arrow-ipc v59.1.0 + Checking arrow v59.1.0 + Compiling crossbeam-utils v0.8.21 + Checking litemap v0.8.2 + Checking writeable v0.6.3 + Checking datafusion-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/common) + Checking icu_locale_core v2.2.0 + Checking futures-channel v0.3.32 + Checking zerotrie v0.2.4 + Checking potential_utf v0.1.5 + Compiling pin-project-internal v1.1.13 + Compiling futures-macro v0.3.32 + Checking slab v0.4.12 + Compiling icu_normalizer_data v2.2.0 + Checking futures-io v0.3.32 + Checking utf8_iter v1.0.4 + Checking futures-task v0.3.32 + Compiling icu_properties_data v2.2.0 + Checking futures-util v0.3.32 + Checking datafusion-expr-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/expr-common) + Checking icu_collections v2.2.0 + Checking pin-project v1.1.13 + Checking icu_provider v2.2.0 + Checking datafusion-physical-expr-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-expr-common) + Checking icu_properties v2.2.0 + Checking icu_normalizer v2.2.0 + Compiling async-trait v0.1.89 + Checking same-file v1.0.6 + Compiling rustix v1.1.4 + Checking walkdir v2.5.0 + Checking idna_adapter v1.2.1 + Checking datafusion-functions-aggregate-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-aggregate-common) + Checking datafusion-functions-window-common v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-window-common) + Checking futures-executor v0.3.32 + Checking percent-encoding v2.3.2 + Compiling thiserror v2.0.18 + Checking linux-raw-sys v0.12.1 + Checking datafusion-doc v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/doc) + Checking datafusion-expr v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/expr) + Checking form_urlencoded v1.2.2 + Checking futures v0.3.32 + Checking idna v1.1.0 + Checking ppv-lite86 v0.2.21 + Checking rand_core v0.9.5 + Checking tracing-core v0.1.36 + Compiling thiserror-impl v2.0.18 + Compiling tracing-attributes v0.1.31 + Compiling crossbeam-epoch v0.9.20 + Checking foldhash v0.1.5 + Checking fastrand v2.4.1 + Checking tempfile v3.27.0 + Checking hashbrown v0.15.5 + Checking tracing v0.1.44 + Checking rand_chacha v0.9.0 + Checking url v2.5.8 + Checking itertools v0.14.0 + Checking http v1.4.0 + Checking hashbrown v0.14.5 + Compiling serde v1.0.228 + Compiling winnow v1.0.2 + Checking fixedbitset v0.5.7 + Checking humantime v2.3.0 + Compiling toml_parser v1.1.2+spec-1.1.0 + Checking object_store v0.13.2 + Checking petgraph v0.8.3 + Checking dashmap v6.2.1 + Compiling datafusion-macros v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/macros) + Checking rand v0.9.4 + Compiling getrandom v0.2.17 + Checking tokio-util v0.7.18 + Compiling serde_derive v1.0.228 + Compiling rayon-core v1.13.0 + Compiling toml_datetime v1.1.1+spec-1.1.0 + Compiling toml_edit v0.25.11+spec-1.1.0 + Checking datafusion-execution v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/execution) + Compiling rand_core v0.6.4 + Checking crossbeam-deque v0.8.6 + Checking datafusion-physical-expr v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-expr) + Compiling rstest_macros v0.26.1 + Compiling alloca v0.4.0 + Checking ciborium-io v0.2.2 + Checking anstyle v1.0.14 + Checking plotters-backend v0.3.7 + Checking clap_lex v1.1.0 + Checking clap_builder v4.6.0 + Checking plotters-svg v0.3.7 + Checking ciborium-ll v0.2.2 + Compiling rand_chacha v0.3.1 + Compiling proc-macro-crate v3.5.0 + Checking itertools v0.13.0 + Checking vte v0.14.1 + Checking bstr v1.12.1 + Checking cast v0.3.0 + Compiling glob v0.3.3 + Checking hex v0.4.3 + Compiling relative-path v1.9.3 + Checking datafusion-functions v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions) + Checking criterion-plot v0.8.2 + Checking globset v0.4.18 + Checking strip-ansi-escapes v0.2.1 + Compiling rand v0.8.6 + Checking rayon v1.12.0 + Checking ciborium v0.2.2 + Checking plotters v0.3.7 + Checking clap v4.6.1 + Checking tinytemplate v1.2.1 + Checking datafusion-common-runtime v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/common-runtime) + Checking console v0.16.3 + Checking page_size v0.6.0 + Checking similar v2.7.0 + Checking futures-timer v3.0.3 + Checking anes v0.1.6 + Checking oorandom v11.1.5 + Checking insta v1.48.0 + Checking criterion v0.8.2 + Checking rstest v0.26.1 + Compiling rstest_reuse v0.7.0 + Checking datafusion-functions-window v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-window) + Checking datafusion-functions-aggregate v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/functions-aggregate) + Checking datafusion-physical-plan v54.0.0 (/home/runner/work/datafusion/datafusion/datafusion/physical-plan) +error: this call to `clone` can be replaced with `std::slice::from_ref` + --> datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs:51:9 + | +51 | &[input_batches.clone()], + | ^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `std::slice::from_ref(&input_batches)` + | + = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.96.0/index.html#cloned_ref_to_slice_refs + = note: `-D clippy::cloned-ref-to-slice-refs` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::cloned_ref_to_slice_refs)]` + +error: could not compile `datafusion-physical-plan` (lib test) due to 1 previous error From de26d4cf2f125a3c2effbeea0b4f9be7e30507d4 Mon Sep 17 00:00:00 2001 From: StealthEyeLLC <5gcrth85jw@privaterelay.appleid.com> Date: Fri, 10 Jul 2026 13:59:22 -0400 Subject: [PATCH 41/41] ci: mirror clippy fix for dictionary aggregation regression --- .../src/aggregates/aggregate_hash_table/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs index 5e22f538a6233..28b765f69a654 100644 --- a/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs +++ b/datafusion/physical-plan/src/aggregates/aggregate_hash_table/tests.rs @@ -48,7 +48,7 @@ fn dictionary_groups_keep_partial_schema_and_promote_final_output() -> Result<() .collect::>>()?; let input = TestMemoryExec::try_new( - &[input_batches.clone()], + std::slice::from_ref(&input_batches), Arc::clone(&input_schema), None, )?; @@ -102,7 +102,7 @@ fn dictionary_groups_keep_partial_schema_and_promote_final_output() -> Result<() ); let partial_input = TestMemoryExec::try_new( - &[partial_batches.clone()], + std::slice::from_ref(&partial_batches), Arc::clone(&partial_schema), None, )?;