This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new 1cfc355796 minor: derive `group_column_supported_type` from 
`make_group_column` (#25541)
1cfc355796 is described below

commit 1cfc35579642210fc826e5a3f24f97c982a62fbc
Author: Jay Zhan <[email protected]>
AuthorDate: Sun Sep 20 13:56:58 2026 +0000

    minor: derive `group_column_supported_type` from `make_group_column` 
(#25541)
    
    ## Which issue does this PR close?
    
    - N/A — small refactor, no issue filed.
    
    ## Rationale for this change
    
    `multi_group_by` keeps the set of types `GroupValuesColumn` supports in
    two places:
    
    - `make_group_column`, the factory that builds a `GroupColumn` for a
    field, and
    - `group_column_supported_type`, a hand-written allow-list that
    `supported_schema` uses to choose between `GroupValuesColumn` and the
    `GroupValuesRows` fallback.
    
    The two must accept exactly the same types: if the allow-list accepts a
    type the factory rejects, the planner picks `GroupValuesColumn` and the
    query then fails with `not_impl_err` when the columns are built. Today
    that is enforced by "keep the two in lockstep" comments on both sides
    (valid `Time32`/`Time64` units, non-negative `FixedSizeBinary` width,
    the nested/row-backed fallback, recursion into `List` and `Dictionary`)
    and by a pinning test over a sample of types. Every new specialization
    has to be added twice.
    
    One case had already drifted: a `Dictionary` with a non-integer key type
    is accepted by the allow-list but rejected by the factory. It is not a
    valid Arrow type, so it is not reachable in practice, but it shows the
    shape of the problem.
    
    ## What changes are included in this PR?
    
    - `group_column_supported_type` now asks the factory:
    `make_group_column(..).is_ok()`. The 50-line allow-list and the lockstep
    comments are removed, so the two cannot disagree. Building a column only
    creates empty buffers, and this runs once per stream, not per batch.
    - The pinning test is kept as the specification of which types take the
    column path and which fall back to rows; its doc comment is updated to
    say so. It gains the drifted `Dictionary(Utf8, Int32)` case, which fails
    on `main` (`expected group_column_supported_type=false`).
    
    No behaviour change for any valid Arrow type.
    
    ## What is the testing strategy for this PR?
    
    Existing tests: `group_column_supported_type_matches_make_group_column`
    (supported and intentionally unsupported types, including nested and
    row-backed cases), the `supported_schema_*` tests and the rest of the
    `multi_group_by` tests.
    
    ## Are there any user-facing changes?
    
    No.
---
 .../aggregates/group_values/multi_group_by/mod.rs  | 105 +++++----------------
 1 file changed, 24 insertions(+), 81 deletions(-)

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 b206fdd94e..5c33abd41d 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
@@ -930,67 +930,17 @@ macro_rules! instantiate_primitive {
     }};
 }
 
-/// Returns true if the specified data type has a specialized
-/// [`GroupColumn`] builder in [`make_group_column`].
+/// Returns true if [`make_group_column`] can build a [`GroupColumn`] for the
+/// specified data type.
 ///
-/// This is the allow-list that gates the `GroupValuesRows` fallback in
-/// [`crate::aggregates::group_values::new_group_values`]: it must accept
-/// exactly the set of types that [`make_group_column`] constructs a
-/// builder for. The `group_column_supported_type_matches_make_group_column`
-/// test below pins this biconditional.
+/// This gates the `GroupValuesRows` fallback in
+/// [`crate::aggregates::group_values::new_group_values`], so it must accept
+/// exactly the types [`make_group_column`] accepts. It asks the factory itself
+/// rather than keeping a second list of types in step with it; building a
+/// column only creates empty buffers, and this runs once per stream.
 fn group_column_supported_type(data_type: &DataType) -> bool {
-    match data_type {
-        DataType::Int8
-            | DataType::Int16
-            | DataType::Int32
-            | DataType::Int64
-            | DataType::UInt8
-            | DataType::UInt16
-            | DataType::UInt32
-            | DataType::UInt64
-            | DataType::Float16
-            | DataType::Float32
-            | DataType::Float64
-            | DataType::Decimal128(_, _)
-            | DataType::Decimal256(_, _)
-            | DataType::Utf8
-            | DataType::LargeUtf8
-            | DataType::Binary
-            | DataType::LargeBinary
-            // Only non-negative widths: a negative width is not a valid
-            // Arrow type (no array can be constructed for it), and the
-            // dispatcher in `make_group_column` rejects it. Keep the two
-            // in lockstep.
-            | DataType::FixedSizeBinary(0..)
-            | DataType::Date32
-            | DataType::Date64
-            // Only the semantically valid Time variants per the Arrow spec.
-            // The dispatcher in `make_group_column` returns NotImpl for the
-            // other unit combinations, so accepting them here would cause a
-            // schema to be routed into GroupValuesColumn and then fail at
-            // intern. Keep these two arms in lockstep with the dispatcher.
-            | DataType::Time32(TimeUnit::Second)
-            | DataType::Time32(TimeUnit::Millisecond)
-            | DataType::Time64(TimeUnit::Microsecond)
-            | DataType::Time64(TimeUnit::Nanosecond)
-            | DataType::Timestamp(_, _)
-            | DataType::Duration(_)
-            | DataType::Interval(_)
-            | DataType::Utf8View
-            | DataType::BinaryView
-            | DataType::Boolean => true,
-        DataType::List(child_field) | DataType::LargeList(child_field)
-            if group_column_supported_type(child_field.data_type()) => true,
-        DataType::Dictionary(_, v) if group_column_supported_type(v) => true,
-        // Nested types (Struct / List / LargeList / FixedSizeList, 
recursively)
-        // that have no type-specialized `GroupColumn` as per the above are 
handled by the generic
-        // row-backed fallback in `make_group_column` whenever arrow's row 
format can
-        // encode them. Gate the fallback to nested types so 
intentionally-excluded
-        // scalar types (e.g. Float16, Decimal256) stay on `GroupValuesRows` 
and the
-        // `group_column_supported_type` ⇔ `make_group_column` invariant holds.
-        dt if dt.is_nested() => RowsGroupColumn::supports_type(dt),
-        _ => false,
-    }
+    // Whether a type is supported does not depend on its nullability
+    make_group_column(&Field::new("", data_type.clone(), true)).is_ok()
 }
 
 /// Build a [`GroupColumn`] for a single schema field.
@@ -1002,12 +952,9 @@ fn group_column_supported_type(data_type: &DataType) -> 
bool {
 /// specializations (e.g. `Struct`, `List`, `LargeList`) plug into without
 /// having to enumerate every combination inline.
 ///
-/// Returns `Err(not_impl_err!(...))` for any type not in the supported set;
-/// callers (`GroupValues::intern`) propagate that error so the
-/// `GroupValuesRows` fallback can take over upstream of this builder.
-///
-/// The allow-list that gates this dispatcher lives in
-/// [`group_column_supported_type`] directly above.
+/// Returns `Err(not_impl_err!(...))` for any type not in the supported set.
+/// [`group_column_supported_type`] reports exactly that, which is how
+/// `new_group_values` knows to use the `GroupValuesRows` fallback instead.
 fn make_group_column(field: &Field) -> Result<Box<dyn GroupColumn>> {
     let nullable = field.is_nullable();
     let data_type = field.data_type();
@@ -1033,8 +980,7 @@ fn make_group_column(field: &Field) -> Result<Box<dyn 
GroupColumn>> {
                 instantiate_primitive!(nullable, Time32MillisecondType, 
data_type)
             }
             // Time32 with Microsecond / Nanosecond is not a valid Arrow type
-            // combination; reject explicitly so group_column_supported_type
-            // and this dispatcher stay in lockstep (see consistency fuzz 
below).
+            // combination; reject explicitly.
             _ => None,
         },
         DataType::Time64(t) => match t {
@@ -1107,8 +1053,8 @@ fn make_group_column(field: &Field) -> Result<Box<dyn 
GroupColumn>> {
         DataType::LargeBinary => 
Some(Box::new(ByteGroupValueBuilder::<i64>::new(
             OutputType::Binary,
         ))),
-        // A negative width is not a valid Arrow type; it falls to the `_`
-        // arm below, matching `group_column_supported_type`.
+        // A negative width is not a valid Arrow type (no array can be
+        // constructed for it); it falls to the `_` arm below.
         DataType::FixedSizeBinary(byte_width @ 0..) => {
             Some(Box::new(FixedSizeBinaryGroupValueBuilder::new(*byte_width)))
         }
@@ -1645,19 +1591,14 @@ mod tests {
         assert_eq!(col_rest.len(), schema.fields().len());
     }
 
-    /// CRITICAL invariant: if `group_column_supported_type(t)` returns true
-    /// the dispatcher must accept that type at intern time, and conversely
-    /// if `group_column_supported_type(t)` returns false the planner must
-    /// NOT route it through `GroupValuesColumn`. A divergence here would
-    /// let the planner select `GroupValuesColumn` for a type whose
-    /// dispatcher arm is missing, producing a runtime `not_impl_err` after
-    /// the field reaches the builder factory.
+    /// Pins which types take the `GroupValuesColumn` path and which fall back
+    /// to `GroupValuesRows`.
     ///
-    /// This test fuzzes a representative cross-section of types and asserts
-    /// both directions of the biconditional. When a new specialization is
-    /// added (`Float16`, `FixedSizeList`, `Struct`, ...) it should be added
-    /// to the supported_cases vector; when a type is intentionally rejected
-    /// it should be added to unsupported_cases.
+    /// `group_column_supported_type` is derived from `make_group_column`, so
+    /// the two cannot disagree; what this test guards is the set itself. When
+    /// a new specialization is added (`Float16`, `FixedSizeList`, `Struct`,
+    /// ...) it should be added to the supported_cases vector; when a type is
+    /// intentionally rejected it should be added to unsupported_cases.
     #[test]
     fn group_column_supported_type_matches_make_group_column() {
         let utf8 = || Field::new("v", DataType::Utf8, true);
@@ -1776,6 +1717,8 @@ mod tests {
             // A negative width is representable in the DataType but is not
             // a valid Arrow type; no array can be constructed for it.
             DataType::FixedSizeBinary(-5),
+            // Likewise a dictionary whose keys are not integers.
+            DataType::Dictionary(Box::new(DataType::Utf8), 
Box::new(DataType::Int32)),
         ];
 
         for dt in &unsupported_cases {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to