diff --git a/datafusion/physical-plan/src/aggregates/group_values/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/mod.rs index f2f489b7223c..316fbe11ae31 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/mod.rs @@ -40,8 +40,8 @@ pub(crate) use single_group_by::primitive::HashValue; use crate::aggregates::{ group_values::single_group_by::{ - bytes::GroupValuesByes, bytes_view::GroupValuesBytesView, - primitive::GroupValuesPrimitive, + boolean::GroupValuesBoolean, bytes::GroupValuesBytes, + bytes_view::GroupValuesBytesView, primitive::GroupValuesPrimitive, }, order::GroupOrdering, }; @@ -119,7 +119,7 @@ pub trait GroupValues: Send { /// - If group by single column, and type of this column has /// the specific [`GroupValues`] implementation, such implementation /// will be chosen. -/// +/// /// - If group by multiple columns, and all column types have the specific /// `GroupColumn` implementations, `GroupValuesColumn` will be chosen. /// @@ -174,23 +174,26 @@ pub fn new_group_values( downcast_helper!(Decimal128Type, d); } DataType::Utf8 => { - return Ok(Box::new(GroupValuesByes::::new(OutputType::Utf8))); + return Ok(Box::new(GroupValuesBytes::::new(OutputType::Utf8))); } DataType::LargeUtf8 => { - return Ok(Box::new(GroupValuesByes::::new(OutputType::Utf8))); + return Ok(Box::new(GroupValuesBytes::::new(OutputType::Utf8))); } DataType::Utf8View => { return Ok(Box::new(GroupValuesBytesView::new(OutputType::Utf8View))); } DataType::Binary => { - return Ok(Box::new(GroupValuesByes::::new(OutputType::Binary))); + return Ok(Box::new(GroupValuesBytes::::new(OutputType::Binary))); } DataType::LargeBinary => { - return Ok(Box::new(GroupValuesByes::::new(OutputType::Binary))); + return Ok(Box::new(GroupValuesBytes::::new(OutputType::Binary))); } DataType::BinaryView => { return Ok(Box::new(GroupValuesBytesView::new(OutputType::BinaryView))); } + DataType::Boolean => { + return Ok(Box::new(GroupValuesBoolean::new())); + } _ => {} } } diff --git a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/boolean.rs b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/boolean.rs new file mode 100644 index 000000000000..91a9e21aeb68 --- /dev/null +++ b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/boolean.rs @@ -0,0 +1,474 @@ +// 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 crate::aggregates::group_values::multi_group_by::{nulls_equal_to, GroupColumn}; +use crate::aggregates::group_values::null_builder::MaybeNullBufferBuilder; +use arrow::array::{Array as _, ArrayRef, AsArray, BooleanArray, BooleanBufferBuilder}; +use datafusion_common::Result; +use itertools::izip; + +/// An implementation of [`GroupColumn`] for booleans +/// +/// Optimized to skip null buffer construction if the input is known to be non nullable +/// +/// # Template parameters +/// +/// `NULLABLE`: if the data can contain any nulls +#[derive(Debug)] +pub struct BooleanGroupValueBuilder { + buffer: BooleanBufferBuilder, + nulls: MaybeNullBufferBuilder, +} + +impl BooleanGroupValueBuilder { + /// Create a new `BooleanGroupValueBuilder` + pub fn new() -> Self { + Self { + buffer: BooleanBufferBuilder::new(0), + nulls: MaybeNullBufferBuilder::new(), + } + } +} + +impl GroupColumn for BooleanGroupValueBuilder { + fn equal_to(&self, lhs_row: usize, array: &ArrayRef, rhs_row: usize) -> bool { + if NULLABLE { + let exist_null = self.nulls.is_null(lhs_row); + let input_null = array.is_null(rhs_row); + if let Some(result) = nulls_equal_to(exist_null, input_null) { + return result; + } + } + + self.buffer.get_bit(lhs_row) == array.as_boolean().value(rhs_row) + } + + fn append_val(&mut self, array: &ArrayRef, row: usize) -> Result<()> { + if NULLABLE { + if array.is_null(row) { + self.nulls.append(true); + self.buffer.append(bool::default()); + } else { + self.nulls.append(false); + self.buffer.append(array.as_boolean().value(row)); + } + } else { + self.buffer.append(array.as_boolean().value(row)); + } + + Ok(()) + } + + fn vectorized_equal_to( + &self, + lhs_rows: &[usize], + array: &ArrayRef, + rhs_rows: &[usize], + equal_to_results: &mut [bool], + ) { + let array = array.as_boolean(); + + let iter = izip!( + lhs_rows.iter(), + rhs_rows.iter(), + equal_to_results.iter_mut(), + ); + + for (&lhs_row, &rhs_row, equal_to_result) in iter { + // Has found not equal to in previous column, don't need to check + if !*equal_to_result { + continue; + } + + if NULLABLE { + let exist_null = self.nulls.is_null(lhs_row); + let input_null = array.is_null(rhs_row); + if let Some(result) = nulls_equal_to(exist_null, input_null) { + *equal_to_result = result; + continue; + } + } + + *equal_to_result = self.buffer.get_bit(lhs_row) == array.value(rhs_row); + } + } + + fn vectorized_append(&mut self, array: &ArrayRef, rows: &[usize]) -> Result<()> { + let arr = array.as_boolean(); + + let null_count = array.null_count(); + let num_rows = array.len(); + let all_null_or_non_null = if null_count == 0 { + Some(true) + } else if null_count == num_rows { + Some(false) + } else { + None + }; + + match (NULLABLE, all_null_or_non_null) { + (true, None) => { + for &row in rows { + if array.is_null(row) { + self.nulls.append(true); + self.buffer.append(bool::default()); + } else { + self.nulls.append(false); + self.buffer.append(arr.value(row)); + } + } + } + + (true, Some(true)) => { + self.nulls.append_n(rows.len(), false); + for &row in rows { + self.buffer.append(arr.value(row)); + } + } + + (true, Some(false)) => { + self.nulls.append_n(rows.len(), true); + self.buffer.append_n(rows.len(), bool::default()); + } + + (false, _) => { + for &row in rows { + self.buffer.append(arr.value(row)); + } + } + } + + Ok(()) + } + + fn len(&self) -> usize { + self.buffer.len() + } + + fn size(&self) -> usize { + self.buffer.capacity() / 8 + self.nulls.allocated_size() + } + + fn build(self: Box) -> ArrayRef { + let Self { mut buffer, nulls } = *self; + + let nulls = nulls.build(); + if !NULLABLE { + assert!(nulls.is_none(), "unexpected nulls in non nullable input"); + } + + let arr = BooleanArray::new(buffer.finish(), nulls); + + Arc::new(arr) + } + + fn take_n(&mut self, n: usize) -> ArrayRef { + let first_n_nulls = if NULLABLE { self.nulls.take_n(n) } else { None }; + + let mut new_builder = BooleanBufferBuilder::new(self.buffer.len()); + new_builder.append_packed_range(n..self.buffer.len(), self.buffer.as_slice()); + std::mem::swap(&mut new_builder, &mut self.buffer); + + // take only first n values from the original builder + new_builder.truncate(n); + + Arc::new(BooleanArray::new(new_builder.finish(), first_n_nulls)) + } +} + +#[cfg(test)] +mod tests { + use arrow::array::NullBufferBuilder; + + use super::*; + + #[test] + fn test_nullable_boolean_equal_to() { + let append = |builder: &mut BooleanGroupValueBuilder, + builder_array: &ArrayRef, + append_rows: &[usize]| { + for &index in append_rows { + builder.append_val(builder_array, index).unwrap(); + } + }; + + let equal_to = |builder: &BooleanGroupValueBuilder, + lhs_rows: &[usize], + input_array: &ArrayRef, + rhs_rows: &[usize], + equal_to_results: &mut Vec| { + let iter = lhs_rows.iter().zip(rhs_rows.iter()); + for (idx, (&lhs_row, &rhs_row)) in iter.enumerate() { + equal_to_results[idx] = builder.equal_to(lhs_row, input_array, rhs_row); + } + }; + + test_nullable_boolean_equal_to_internal(append, equal_to); + } + + #[test] + fn test_nullable_primitive_vectorized_equal_to() { + let append = |builder: &mut BooleanGroupValueBuilder, + builder_array: &ArrayRef, + append_rows: &[usize]| { + builder + .vectorized_append(builder_array, append_rows) + .unwrap(); + }; + + let equal_to = |builder: &BooleanGroupValueBuilder, + lhs_rows: &[usize], + input_array: &ArrayRef, + rhs_rows: &[usize], + equal_to_results: &mut Vec| { + builder.vectorized_equal_to( + lhs_rows, + input_array, + rhs_rows, + equal_to_results, + ); + }; + + test_nullable_boolean_equal_to_internal(append, equal_to); + } + + fn test_nullable_boolean_equal_to_internal(mut append: A, mut equal_to: E) + where + A: FnMut(&mut BooleanGroupValueBuilder, &ArrayRef, &[usize]), + E: FnMut( + &BooleanGroupValueBuilder, + &[usize], + &ArrayRef, + &[usize], + &mut Vec, + ), + { + // Will cover such cases: + // - exist null, input not null + // - exist null, input null; values not equal + // - exist null, input null; values equal + // - exist not null, input null + // - exist not null, input not null; values not equal + // - exist not null, input not null; values equal + + // Define PrimitiveGroupValueBuilder + let mut builder = BooleanGroupValueBuilder::::new(); + let builder_array = Arc::new(BooleanArray::from(vec![ + None, + None, + None, + Some(true), + Some(false), + Some(true), + ])) as ArrayRef; + append(&mut builder, &builder_array, &[0, 1, 2, 3, 4, 5]); + + // Define input array + let (values, _nulls) = BooleanArray::from(vec![ + Some(true), + Some(false), + None, + None, + Some(true), + Some(true), + ]) + .into_parts(); + + // explicitly build a null buffer where one of the null values also happens to match + let mut nulls = NullBufferBuilder::new(6); + nulls.append_non_null(); + nulls.append_null(); // this sets Some(false) to null above + nulls.append_null(); + nulls.append_null(); + nulls.append_non_null(); + nulls.append_non_null(); + let input_array = Arc::new(BooleanArray::new(values, nulls.finish())) as ArrayRef; + + // Check + let mut equal_to_results = vec![true; builder.len()]; + equal_to( + &builder, + &[0, 1, 2, 3, 4, 5], + &input_array, + &[0, 1, 2, 3, 4, 5], + &mut equal_to_results, + ); + + assert!(!equal_to_results[0]); + assert!(equal_to_results[1]); + assert!(equal_to_results[2]); + assert!(!equal_to_results[3]); + assert!(!equal_to_results[4]); + assert!(equal_to_results[5]); + } + + #[test] + fn test_not_nullable_primitive_equal_to() { + let append = |builder: &mut BooleanGroupValueBuilder, + builder_array: &ArrayRef, + append_rows: &[usize]| { + for &index in append_rows { + builder.append_val(builder_array, index).unwrap(); + } + }; + + let equal_to = |builder: &BooleanGroupValueBuilder, + lhs_rows: &[usize], + input_array: &ArrayRef, + rhs_rows: &[usize], + equal_to_results: &mut Vec| { + let iter = lhs_rows.iter().zip(rhs_rows.iter()); + for (idx, (&lhs_row, &rhs_row)) in iter.enumerate() { + equal_to_results[idx] = builder.equal_to(lhs_row, input_array, rhs_row); + } + }; + + test_not_nullable_boolean_equal_to_internal(append, equal_to); + } + + #[test] + fn test_not_nullable_primitive_vectorized_equal_to() { + let append = |builder: &mut BooleanGroupValueBuilder, + builder_array: &ArrayRef, + append_rows: &[usize]| { + builder + .vectorized_append(builder_array, append_rows) + .unwrap(); + }; + + let equal_to = |builder: &BooleanGroupValueBuilder, + lhs_rows: &[usize], + input_array: &ArrayRef, + rhs_rows: &[usize], + equal_to_results: &mut Vec| { + builder.vectorized_equal_to( + lhs_rows, + input_array, + rhs_rows, + equal_to_results, + ); + }; + + test_not_nullable_boolean_equal_to_internal(append, equal_to); + } + + fn test_not_nullable_boolean_equal_to_internal(mut append: A, mut equal_to: E) + where + A: FnMut(&mut BooleanGroupValueBuilder, &ArrayRef, &[usize]), + E: FnMut( + &BooleanGroupValueBuilder, + &[usize], + &ArrayRef, + &[usize], + &mut Vec, + ), + { + // Will cover such cases: + // - values equal + // - values not equal + + // Define PrimitiveGroupValueBuilder + let mut builder = BooleanGroupValueBuilder::::new(); + let builder_array = Arc::new(BooleanArray::from(vec![ + Some(false), + Some(true), + Some(false), + Some(true), + ])) as ArrayRef; + append(&mut builder, &builder_array, &[0, 1, 2, 3]); + + // Define input array + let input_array = Arc::new(BooleanArray::from(vec![ + Some(false), + Some(false), + Some(true), + Some(true), + ])) as ArrayRef; + + // Check + let mut equal_to_results = vec![true; builder.len()]; + equal_to( + &builder, + &[0, 1, 2, 3], + &input_array, + &[0, 1, 2, 3], + &mut equal_to_results, + ); + + assert!(equal_to_results[0]); + assert!(!equal_to_results[1]); + assert!(!equal_to_results[2]); + assert!(equal_to_results[3]); + } + + #[test] + fn test_nullable_boolean_vectorized_operation_special_case() { + // Test the special `all nulls` or `not nulls` input array case + // for vectorized append and equal to + + let mut builder = BooleanGroupValueBuilder::::new(); + + // All nulls input array + let all_nulls_input_array = + Arc::new(BooleanArray::from(vec![None, None, None, None, None])) as _; + builder + .vectorized_append(&all_nulls_input_array, &[0, 1, 2, 3, 4]) + .unwrap(); + + let mut equal_to_results = vec![true; all_nulls_input_array.len()]; + builder.vectorized_equal_to( + &[0, 1, 2, 3, 4], + &all_nulls_input_array, + &[0, 1, 2, 3, 4], + &mut equal_to_results, + ); + + assert!(equal_to_results[0]); + assert!(equal_to_results[1]); + assert!(equal_to_results[2]); + assert!(equal_to_results[3]); + assert!(equal_to_results[4]); + + // All not nulls input array + let all_not_nulls_input_array = Arc::new(BooleanArray::from(vec![ + Some(false), + Some(true), + Some(false), + Some(true), + Some(true), + ])) as _; + builder + .vectorized_append(&all_not_nulls_input_array, &[0, 1, 2, 3, 4]) + .unwrap(); + + let mut equal_to_results = vec![true; all_not_nulls_input_array.len()]; + builder.vectorized_equal_to( + &[5, 6, 7, 8, 9], + &all_not_nulls_input_array, + &[0, 1, 2, 3, 4], + &mut equal_to_results, + ); + + assert!(equal_to_results[0]); + assert!(equal_to_results[1]); + assert!(equal_to_results[2]); + assert!(equal_to_results[3]); + assert!(equal_to_results[4]); + } +} diff --git a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/bytes.rs b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/bytes.rs index be1f68ea453f..87528dc00135 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/bytes.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/bytes.rs @@ -633,7 +633,7 @@ mod tests { // - exist not null, input not null; values not equal // - exist not null, input not null; values equal - // Define PrimitiveGroupValueBuilder + // Define ByteGroupValueBuilder let mut builder = ByteGroupValueBuilder::::new(OutputType::Utf8); let builder_array = Arc::new(StringArray::from(vec![ None, diff --git a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs index 5d96ac6dcced..ccdcbb13bf22 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/mod.rs @@ -17,6 +17,7 @@ //! `GroupValues` implementations for multi group by cases +mod boolean; mod bytes; pub mod bytes_view; mod primitive; @@ -24,8 +25,8 @@ mod primitive; use std::mem::{self, size_of}; use crate::aggregates::group_values::multi_group_by::{ - bytes::ByteGroupValueBuilder, bytes_view::ByteViewGroupValueBuilder, - primitive::PrimitiveGroupValueBuilder, + boolean::BooleanGroupValueBuilder, bytes::ByteGroupValueBuilder, + bytes_view::ByteViewGroupValueBuilder, primitive::PrimitiveGroupValueBuilder, }; use crate::aggregates::group_values::GroupValues; use ahash::RandomState; @@ -1047,6 +1048,15 @@ impl GroupValues for GroupValuesColumn { let b = ByteViewGroupValueBuilder::::new(); v.push(Box::new(b) as _) } + &DataType::Boolean => { + if nullable { + let b = BooleanGroupValueBuilder::::new(); + v.push(Box::new(b) as _) + } else { + let b = BooleanGroupValueBuilder::::new(); + v.push(Box::new(b) as _) + } + } dt => { return not_impl_err!("{dt} not supported in GroupValuesColumn") } @@ -1236,6 +1246,7 @@ fn supported_type(data_type: &DataType) -> bool { | DataType::Timestamp(_, _) | DataType::Utf8View | DataType::BinaryView + | DataType::Boolean ) } diff --git a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs index afec25fd3d66..f560121cd79e 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs @@ -305,7 +305,7 @@ mod tests { append(&mut builder, &builder_array, &[0, 1, 2, 3, 4, 5]); // Define input array - let (_nulls, values, _) = + let (_, values, _nulls) = Int64Array::from(vec![Some(1), Some(2), None, None, Some(1), Some(3)]) .into_parts(); diff --git a/datafusion/physical-plan/src/aggregates/group_values/single_group_by/boolean.rs b/datafusion/physical-plan/src/aggregates/group_values/single_group_by/boolean.rs new file mode 100644 index 000000000000..44b763a91f52 --- /dev/null +++ b/datafusion/physical-plan/src/aggregates/group_values/single_group_by/boolean.rs @@ -0,0 +1,154 @@ +// 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 crate::aggregates::group_values::GroupValues; + +use arrow::array::{ + ArrayRef, AsArray as _, BooleanArray, BooleanBufferBuilder, NullBufferBuilder, + RecordBatch, +}; +use datafusion_common::Result; +use datafusion_expr::EmitTo; +use std::{mem::size_of, sync::Arc}; + +#[derive(Debug)] +pub struct GroupValuesBoolean { + false_group: Option, + true_group: Option, + null_group: Option, +} + +impl GroupValuesBoolean { + pub fn new() -> Self { + Self { + false_group: None, + true_group: None, + null_group: None, + } + } +} + +impl GroupValues for GroupValuesBoolean { + fn intern(&mut self, cols: &[ArrayRef], groups: &mut Vec) -> Result<()> { + let array = cols[0].as_boolean(); + groups.clear(); + + for value in array.iter() { + let index = match value { + Some(false) => { + if let Some(index) = self.false_group { + index + } else { + let index = self.len(); + self.false_group = Some(index); + index + } + } + Some(true) => { + if let Some(index) = self.true_group { + index + } else { + let index = self.len(); + self.true_group = Some(index); + index + } + } + None => { + if let Some(index) = self.null_group { + index + } else { + let index = self.len(); + self.null_group = Some(index); + index + } + } + }; + + groups.push(index); + } + + Ok(()) + } + + fn size(&self) -> usize { + size_of::() + } + + fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn len(&self) -> usize { + self.false_group.is_some() as usize + + self.true_group.is_some() as usize + + self.null_group.is_some() as usize + } + + fn emit(&mut self, emit_to: EmitTo) -> Result> { + let len = self.len(); + let mut builder = BooleanBufferBuilder::new(len); + let emit_count = match emit_to { + EmitTo::All => len, + EmitTo::First(n) => n, + }; + builder.append_n(emit_count, false); + if let Some(idx) = self.true_group.as_mut() { + if *idx < emit_count { + builder.set_bit(*idx, true); + self.true_group = None; + } else { + *idx -= emit_count; + } + } + + if let Some(idx) = self.false_group.as_mut() { + if *idx < emit_count { + // already false, no need to set + self.false_group = None; + } else { + *idx -= emit_count; + } + } + + let values = builder.finish(); + + let nulls = if let Some(idx) = self.null_group.as_mut() { + if *idx < emit_count { + let mut buffer = NullBufferBuilder::new(len); + buffer.append_n_non_nulls(*idx); + buffer.append_null(); + buffer.append_n_non_nulls(emit_count - *idx - 1); + + self.null_group = None; + Some(buffer.finish().unwrap()) + } else { + *idx -= emit_count; + None + } + } else { + None + }; + + Ok(vec![Arc::new(BooleanArray::new(values, nulls)) as _]) + } + + fn clear_shrink(&mut self, _batch: &RecordBatch) { + self.false_group = None; + self.true_group = None; + self.null_group = None; + } +} diff --git a/datafusion/physical-plan/src/aggregates/group_values/single_group_by/bytes.rs b/datafusion/physical-plan/src/aggregates/group_values/single_group_by/bytes.rs index 21078ceb8aed..b901aee313fb 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/single_group_by/bytes.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/single_group_by/bytes.rs @@ -28,14 +28,14 @@ use datafusion_physical_expr_common::binary_map::{ArrowBytesMap, OutputType}; /// /// This specialization is significantly faster than using the more general /// purpose `Row`s format -pub struct GroupValuesByes { +pub struct GroupValuesBytes { /// Map string/binary values to group index map: ArrowBytesMap, /// The total number of groups so far (used to assign group_index) num_groups: usize, } -impl GroupValuesByes { +impl GroupValuesBytes { pub fn new(output_type: OutputType) -> Self { Self { map: ArrowBytesMap::new(output_type), @@ -44,7 +44,7 @@ impl GroupValuesByes { } } -impl GroupValues for GroupValuesByes { +impl GroupValues for GroupValuesBytes { fn intern(&mut self, cols: &[ArrayRef], groups: &mut Vec) -> Result<()> { assert_eq!(cols.len(), 1); diff --git a/datafusion/physical-plan/src/aggregates/group_values/single_group_by/mod.rs b/datafusion/physical-plan/src/aggregates/group_values/single_group_by/mod.rs index 417618ba66af..89c6b624e8e0 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/single_group_by/mod.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/single_group_by/mod.rs @@ -17,6 +17,7 @@ //! `GroupValues` implementations for single group by cases +pub(crate) mod boolean; pub(crate) mod bytes; pub(crate) mod bytes_view; pub(crate) mod primitive;