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

Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 3782011c240 Improve MIN/MAX error message for non-numeric columns 
(#19653)
3782011c240 is described below

commit 3782011c240fa91b3df64151cc4186dfaa377fe0
Author: akashchamp <[email protected]>
AuthorDate: Tue Oct 13 02:13:07 2026 +0530

    Improve MIN/MAX error message for non-numeric columns (#19653)
---
 .../function/AggregationFunctionUtils.java         |  16 +++-
 .../function/MaxAggregationFunction.java           |  53 +++++++----
 .../function/MinAggregationFunction.java           |  53 +++++++----
 .../function/AggregationFunctionUtilsTest.java     |  33 +++++++
 .../function/MaxAggregationFunctionTest.java       | 105 +++++++++++++++++++++
 .../function/MinAggregationFunctionTest.java       | 105 +++++++++++++++++++++
 6 files changed, 332 insertions(+), 33 deletions(-)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
index fb59e4dd00e..07c433112a6 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtils.java
@@ -74,6 +74,7 @@ import 
org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
 import org.apache.pinot.segment.spi.index.reader.Dictionary;
 import 
org.apache.pinot.segment.spi.index.startree.AggregationFunctionColumnPair;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
 import org.apache.pinot.spi.query.QueryThreadContext;
 import org.apache.pinot.spi.utils.ByteArray;
 
@@ -738,13 +739,26 @@ public class AggregationFunctionUtils {
     return ((Number) 
dataSource.getDataSourceMetadata().getMaxValue()).longValue();
   }
 
+  /// Converts the given dictionary/metadata min/max value to a `double`, for 
the numeric MIN/MAX/MINMAXRANGE
+  /// dictionary/metadata based resolution (see [#getMinValueNumeric] and 
[#getMaxValueNumeric]).
+  ///
+  /// @throws BadQueryRequestException if `value` is not a [Number] and cannot 
be parsed as one, which happens when
+  /// MIN/MAX is applied to a non-numeric (e.g. STRING) column; the message 
points the caller to MINSTRING/MAXSTRING
+  /// and the `autoRewriteAggregationType` query option instead of surfacing a 
raw [NumberFormatException].
   private static Double toDouble(Comparable<?> value) {
     if (value instanceof Double) {
       return (Double) value;
     } else if (value instanceof Number) {
       return ((Number) value).doubleValue();
-    } else {
+    }
+    try {
       return Double.parseDouble(value.toString());
+    } catch (NumberFormatException e) {
+      throw new BadQueryRequestException(
+          "Cannot compute MIN/MAX on non-numeric value: \"" + value + "\". 
MIN/MAX only support numeric columns; "
+              + "use MINSTRING/MAXSTRING for string columns, or set the query 
option "
+              + "'autoRewriteAggregationType=true' to automatically rewrite 
MIN/MAX to MINSTRING/MAXSTRING on "
+              + "string columns.", e);
     }
   }
 
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxAggregationFunction.java
index a559c301d8b..e50341a96fb 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxAggregationFunction.java
@@ -37,6 +37,9 @@ import 
org.apache.pinot.spi.exception.BadQueryRequestException;
 
 public class MaxAggregationFunction extends 
BaseSingleInputAggregationFunction<Double, Double> {
   protected static final double DEFAULT_INITIAL_VALUE = 
Double.NEGATIVE_INFINITY;
+  private static final String NON_NUMERIC_TYPE_MESSAGE_SUFFIX = ". MAX only 
supports numeric columns; use MAXSTRING "
+      + "for string columns, or set the query option 
'autoRewriteAggregationType=true' to automatically rewrite "
+      + "MAX to MAXSTRING on string columns.";
 
   public MaxAggregationFunction(List<ExpressionContext> arguments, boolean 
nullHandlingEnabled) {
     this(verifySingleArgument(arguments, "MAX"), nullHandlingEnabled);
@@ -71,14 +74,25 @@ public class MaxAggregationFunction extends 
BaseSingleInputAggregationFunction<D
   public void aggregate(int length, AggregationResultHolder 
aggregationResultHolder,
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
-
-    if (blockValSet.isSingleValue()) {
-      aggregateSV(blockValSet, length, aggregationResultHolder);
-    } else {
-      aggregateMV(blockValSet, length, aggregationResultHolder);
+    try {
+      if (blockValSet.isSingleValue()) {
+        aggregateSV(blockValSet, length, aggregationResultHolder);
+      } else {
+        aggregateMV(blockValSet, length, aggregationResultHolder);
+      }
+    } catch (NumberFormatException e) {
+      throw nonNumericValueException(blockValSet, e);
     }
   }
 
+  /// Wraps a failure to convert a non-numeric (e.g. STRING) value to a 
`double` into a descriptive
+  /// [BadQueryRequestException]. Numeric strings still convert successfully 
and are not affected.
+  private static BadQueryRequestException nonNumericValueException(BlockValSet 
blockValSet, NumberFormatException e) {
+    return new BadQueryRequestException(
+        "Cannot compute max for non-numeric value in column of type: " + 
blockValSet.getValueType()
+            + NON_NUMERIC_TYPE_MESSAGE_SUFFIX + " Cause: " + e.getMessage(), 
e);
+  }
+
   protected void aggregateSV(BlockValSet blockValSet, int length, 
AggregationResultHolder aggregationResultHolder) {
     switch (blockValSet.getValueType().getStoredType()) {
       case INT: {
@@ -153,7 +167,8 @@ public class MaxAggregationFunction extends 
BaseSingleInputAggregationFunction<D
         break;
       }
       default:
-        throw new BadQueryRequestException("Cannot compute max for non-numeric 
type: " + blockValSet.getValueType());
+        throw new BadQueryRequestException(
+            "Cannot compute max for non-numeric type: " + 
blockValSet.getValueType() + NON_NUMERIC_TYPE_MESSAGE_SUFFIX);
     }
   }
 
@@ -191,11 +206,14 @@ public class MaxAggregationFunction extends 
BaseSingleInputAggregationFunction<D
   public void aggregateGroupBySV(int length, int[] groupKeyArray, 
GroupByResultHolder groupByResultHolder,
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
-
-    if (blockValSet.isSingleValue()) {
-      aggregateSVGroupBySV(blockValSet, length, groupKeyArray, 
groupByResultHolder);
-    } else {
-      aggregateMVGroupBySV(blockValSet, length, groupKeyArray, 
groupByResultHolder);
+    try {
+      if (blockValSet.isSingleValue()) {
+        aggregateSVGroupBySV(blockValSet, length, groupKeyArray, 
groupByResultHolder);
+      } else {
+        aggregateMVGroupBySV(blockValSet, length, groupKeyArray, 
groupByResultHolder);
+      }
+    } catch (NumberFormatException e) {
+      throw nonNumericValueException(blockValSet, e);
     }
   }
 
@@ -260,11 +278,14 @@ public class MaxAggregationFunction extends 
BaseSingleInputAggregationFunction<D
   public void aggregateGroupByMV(int length, int[][] groupKeysArray, 
GroupByResultHolder groupByResultHolder,
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
-
-    if (blockValSet.isSingleValue()) {
-      aggregateSVGroupByMV(blockValSet, length, groupKeysArray, 
groupByResultHolder);
-    } else {
-      aggregateMVGroupByMV(blockValSet, length, groupKeysArray, 
groupByResultHolder);
+    try {
+      if (blockValSet.isSingleValue()) {
+        aggregateSVGroupByMV(blockValSet, length, groupKeysArray, 
groupByResultHolder);
+      } else {
+        aggregateMVGroupByMV(blockValSet, length, groupKeysArray, 
groupByResultHolder);
+      }
+    } catch (NumberFormatException e) {
+      throw nonNumericValueException(blockValSet, e);
     }
   }
 
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinAggregationFunction.java
index c002bf0d290..80544112d0d 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinAggregationFunction.java
@@ -37,6 +37,9 @@ import 
org.apache.pinot.spi.exception.BadQueryRequestException;
 
 public class MinAggregationFunction extends 
BaseSingleInputAggregationFunction<Double, Double> {
   protected static final double DEFAULT_VALUE = Double.POSITIVE_INFINITY;
+  private static final String NON_NUMERIC_TYPE_MESSAGE_SUFFIX = ". MIN only 
supports numeric columns; use MINSTRING "
+      + "for string columns, or set the query option 
'autoRewriteAggregationType=true' to automatically rewrite "
+      + "MIN to MINSTRING on string columns.";
 
   public MinAggregationFunction(List<ExpressionContext> arguments, boolean 
nullHandlingEnabled) {
     this(verifySingleArgument(arguments, "MIN"), nullHandlingEnabled);
@@ -71,14 +74,25 @@ public class MinAggregationFunction extends 
BaseSingleInputAggregationFunction<D
   public void aggregate(int length, AggregationResultHolder 
aggregationResultHolder,
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
-
-    if (blockValSet.isSingleValue()) {
-      aggregateSV(blockValSet, length, aggregationResultHolder);
-    } else {
-      aggregateMV(blockValSet, length, aggregationResultHolder);
+    try {
+      if (blockValSet.isSingleValue()) {
+        aggregateSV(blockValSet, length, aggregationResultHolder);
+      } else {
+        aggregateMV(blockValSet, length, aggregationResultHolder);
+      }
+    } catch (NumberFormatException e) {
+      throw nonNumericValueException(blockValSet, e);
     }
   }
 
+  /// Wraps a failure to convert a non-numeric (e.g. STRING) value to a 
`double` into a descriptive
+  /// [BadQueryRequestException]. Numeric strings still convert successfully 
and are not affected.
+  private static BadQueryRequestException nonNumericValueException(BlockValSet 
blockValSet, NumberFormatException e) {
+    return new BadQueryRequestException(
+        "Cannot compute min for non-numeric value in column of type: " + 
blockValSet.getValueType()
+            + NON_NUMERIC_TYPE_MESSAGE_SUFFIX + " Cause: " + e.getMessage(), 
e);
+  }
+
   protected void aggregateSV(BlockValSet blockValSet, int length, 
AggregationResultHolder aggregationResultHolder) {
     switch (blockValSet.getValueType().getStoredType()) {
       case INT: {
@@ -153,7 +167,8 @@ public class MinAggregationFunction extends 
BaseSingleInputAggregationFunction<D
         break;
       }
       default:
-        throw new BadQueryRequestException("Cannot compute min for non-numeric 
type: " + blockValSet.getValueType());
+        throw new BadQueryRequestException(
+            "Cannot compute min for non-numeric type: " + 
blockValSet.getValueType() + NON_NUMERIC_TYPE_MESSAGE_SUFFIX);
     }
   }
 
@@ -191,11 +206,14 @@ public class MinAggregationFunction extends 
BaseSingleInputAggregationFunction<D
   public void aggregateGroupBySV(int length, int[] groupKeyArray, 
GroupByResultHolder groupByResultHolder,
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
-
-    if (blockValSet.isSingleValue()) {
-      aggregateSVGroupBySV(blockValSet, length, groupKeyArray, 
groupByResultHolder);
-    } else {
-      aggregateMVGroupBySV(blockValSet, length, groupKeyArray, 
groupByResultHolder);
+    try {
+      if (blockValSet.isSingleValue()) {
+        aggregateSVGroupBySV(blockValSet, length, groupKeyArray, 
groupByResultHolder);
+      } else {
+        aggregateMVGroupBySV(blockValSet, length, groupKeyArray, 
groupByResultHolder);
+      }
+    } catch (NumberFormatException e) {
+      throw nonNumericValueException(blockValSet, e);
     }
   }
 
@@ -260,11 +278,14 @@ public class MinAggregationFunction extends 
BaseSingleInputAggregationFunction<D
   public void aggregateGroupByMV(int length, int[][] groupKeysArray, 
GroupByResultHolder groupByResultHolder,
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
-
-    if (blockValSet.isSingleValue()) {
-      aggregateSVGroupByMV(blockValSet, length, groupKeysArray, 
groupByResultHolder);
-    } else {
-      aggregateMVGroupByMV(blockValSet, length, groupKeysArray, 
groupByResultHolder);
+    try {
+      if (blockValSet.isSingleValue()) {
+        aggregateSVGroupByMV(blockValSet, length, groupKeysArray, 
groupByResultHolder);
+      } else {
+        aggregateMVGroupByMV(blockValSet, length, groupKeysArray, 
groupByResultHolder);
+      }
+    } catch (NumberFormatException e) {
+      throw nonNumericValueException(blockValSet, e);
     }
   }
 
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtilsTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtilsTest.java
index 29041933af2..53cd082fc0c 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtilsTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionUtilsTest.java
@@ -21,12 +21,15 @@ package org.apache.pinot.core.query.aggregation.function;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.index.reader.Dictionary;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
 import org.testng.annotations.Test;
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
 
 /// Unit test for {@link AggregationFunctionUtils#getAggregationResult}, the 
metadata/dictionary based aggregation
@@ -65,6 +68,36 @@ public class AggregationFunctionUtilsTest {
     assertEquals(maxResult, 10.0);
   }
 
+  @Test
+  public void testMinAndMaxOnNonNumericDictionaryThrows() {
+    // Reproduces https://github.com/apache/pinot/issues/19603: MIN/MAX 
resolved from a dictionary-encoded
+    // non-numeric (e.g. STRING) column must fail with a clear, actionable 
message instead of letting an opaque
+    // NumberFormatException escape from Double.parseDouble.
+    Dictionary dictionary = mock(Dictionary.class);
+    when(dictionary.getMinVal()).thenReturn("2013-07-01");
+    when(dictionary.getMaxVal()).thenReturn("2013-07-01");
+    DataSource dataSource = mock(DataSource.class);
+    when(dataSource.getDictionary()).thenReturn(dictionary);
+
+    try {
+      
AggregationFunctionUtils.getAggregationResult(mockFunction(AggregationFunctionType.MIN),
 dataSource, 100,
+          "TEST");
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("MINSTRING"));
+      assertTrue(e.getMessage().contains("autoRewriteAggregationType"));
+    }
+
+    try {
+      
AggregationFunctionUtils.getAggregationResult(mockFunction(AggregationFunctionType.MAX),
 dataSource, 100,
+          "TEST");
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("MAXSTRING"));
+      assertTrue(e.getMessage().contains("autoRewriteAggregationType"));
+    }
+  }
+
   @Test
   public void testUnsupportedFunctionThrows() {
     // MODE cannot be resolved from dictionary/metadata; the resolver must 
reject it rather than return a wrong result.
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxAggregationFunctionTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxAggregationFunctionTest.java
index d74ffd9a70b..480c379709f 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxAggregationFunctionTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxAggregationFunctionTest.java
@@ -18,17 +18,122 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.request.context.RequestContextUtils;
+import org.apache.pinot.core.common.BlockValSet;
+import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
+import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
 import org.apache.pinot.queries.FluentQueryTest;
 import org.apache.pinot.spi.data.FieldSpec;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.apache.pinot.spi.data.FieldSpec.FieldType;
 import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
 
 public class MaxAggregationFunctionTest extends 
AbstractAggregationFunctionTest {
 
+  /// MAX only supports numeric columns (see 
https://github.com/apache/pinot/issues/19603); applying it to a
+  /// non-numeric (e.g. STRING) column must fail with a clear, actionable 
message instead of an opaque
+  /// `NumberFormatException`.
+  @Test
+  public void testNonNumericColumnExceptionInAggregateMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MaxAggregationFunction function = new 
MaxAggregationFunction(List.of(expression), false);
+
+    AggregationResultHolder resultHolder = 
function.createAggregationResultHolder();
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(DataType.STRING);
+    when(mockBlockValSet.isSingleValue()).thenReturn(true);
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    try {
+      function.aggregate(10, resultHolder, blockValSetMap);
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("Cannot compute max for non-numeric 
type"));
+      assertTrue(e.getMessage().contains("MAXSTRING"));
+      assertTrue(e.getMessage().contains("autoRewriteAggregationType"));
+    }
+  }
+
+  @Test
+  public void testNonNumericColumnExceptionInAggregateGroupBySVMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MaxAggregationFunction function = new 
MaxAggregationFunction(List.of(expression), false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(DataType.STRING);
+    when(mockBlockValSet.isSingleValue()).thenReturn(true);
+    when(mockBlockValSet.getDoubleValuesSV()).thenThrow(new 
NumberFormatException("For input string: \"abc\""));
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    try {
+      function.aggregateGroupBySV(10, new int[10], groupByResultHolder, 
blockValSetMap);
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("Cannot compute max for non-numeric 
value"));
+      assertTrue(e.getMessage().contains("MAXSTRING"));
+      assertTrue(e.getMessage().contains("autoRewriteAggregationType"));
+    }
+  }
+
+  @Test
+  public void testNonNumericColumnExceptionInAggregateGroupByMVMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MaxAggregationFunction function = new 
MaxAggregationFunction(List.of(expression), false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(DataType.STRING);
+    when(mockBlockValSet.isSingleValue()).thenReturn(true);
+    when(mockBlockValSet.getDoubleValuesSV()).thenThrow(new 
NumberFormatException("For input string: \"abc\""));
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    try {
+      function.aggregateGroupByMV(10, new int[10][], groupByResultHolder, 
blockValSetMap);
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("Cannot compute max for non-numeric 
value"));
+      assertTrue(e.getMessage().contains("MAXSTRING"));
+      assertTrue(e.getMessage().contains("autoRewriteAggregationType"));
+    }
+  }
+
+  /// A STRING column holding numeric values keeps working on the group-by 
path, since `getDoubleValuesSV()` parses
+  /// them; only values that cannot be parsed are reported with the new 
message.
+  @Test
+  public void testNumericStringColumnStillAggregatesInGroupBy() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MaxAggregationFunction function = new 
MaxAggregationFunction(List.of(expression), false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(DataType.STRING);
+    when(mockBlockValSet.isSingleValue()).thenReturn(true);
+    when(mockBlockValSet.getDoubleValuesSV()).thenReturn(new double[]{2, 10});
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    function.aggregateGroupBySV(2, new int[]{0, 0}, groupByResultHolder, 
blockValSetMap);
+    assertEquals(groupByResultHolder.getDoubleResult(0), 10.0);
+  }
+
   @DataProvider(name = "scenarios")
   Object[] scenarios() {
     return new Object[] {
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinAggregationFunctionTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinAggregationFunctionTest.java
index 3ad5beec039..b20df3da57e 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinAggregationFunctionTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinAggregationFunctionTest.java
@@ -18,17 +18,122 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.request.context.RequestContextUtils;
+import org.apache.pinot.core.common.BlockValSet;
+import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
+import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
 import org.apache.pinot.queries.FluentQueryTest;
 import org.apache.pinot.spi.data.FieldSpec;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.apache.pinot.spi.data.FieldSpec.FieldType;
 import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
 
 public class MinAggregationFunctionTest extends 
AbstractAggregationFunctionTest {
 
+  /// MIN only supports numeric columns (see 
https://github.com/apache/pinot/issues/19603); applying it to a
+  /// non-numeric (e.g. STRING) column must fail with a clear, actionable 
message instead of an opaque
+  /// `NumberFormatException`.
+  @Test
+  public void testNonNumericColumnExceptionInAggregateMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MinAggregationFunction function = new 
MinAggregationFunction(List.of(expression), false);
+
+    AggregationResultHolder resultHolder = 
function.createAggregationResultHolder();
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(DataType.STRING);
+    when(mockBlockValSet.isSingleValue()).thenReturn(true);
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    try {
+      function.aggregate(10, resultHolder, blockValSetMap);
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("Cannot compute min for non-numeric 
type"));
+      assertTrue(e.getMessage().contains("MINSTRING"));
+      assertTrue(e.getMessage().contains("autoRewriteAggregationType"));
+    }
+  }
+
+  @Test
+  public void testNonNumericColumnExceptionInAggregateGroupBySVMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MinAggregationFunction function = new 
MinAggregationFunction(List.of(expression), false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(DataType.STRING);
+    when(mockBlockValSet.isSingleValue()).thenReturn(true);
+    when(mockBlockValSet.getDoubleValuesSV()).thenThrow(new 
NumberFormatException("For input string: \"abc\""));
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    try {
+      function.aggregateGroupBySV(10, new int[10], groupByResultHolder, 
blockValSetMap);
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("Cannot compute min for non-numeric 
value"));
+      assertTrue(e.getMessage().contains("MINSTRING"));
+      assertTrue(e.getMessage().contains("autoRewriteAggregationType"));
+    }
+  }
+
+  @Test
+  public void testNonNumericColumnExceptionInAggregateGroupByMVMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MinAggregationFunction function = new 
MinAggregationFunction(List.of(expression), false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(DataType.STRING);
+    when(mockBlockValSet.isSingleValue()).thenReturn(true);
+    when(mockBlockValSet.getDoubleValuesSV()).thenThrow(new 
NumberFormatException("For input string: \"abc\""));
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    try {
+      function.aggregateGroupByMV(10, new int[10][], groupByResultHolder, 
blockValSetMap);
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("Cannot compute min for non-numeric 
value"));
+      assertTrue(e.getMessage().contains("MINSTRING"));
+      assertTrue(e.getMessage().contains("autoRewriteAggregationType"));
+    }
+  }
+
+  /// A STRING column holding numeric values keeps working on the group-by 
path, since `getDoubleValuesSV()` parses
+  /// them; only values that cannot be parsed are reported with the new 
message.
+  @Test
+  public void testNumericStringColumnStillAggregatesInGroupBy() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MinAggregationFunction function = new 
MinAggregationFunction(List.of(expression), false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(DataType.STRING);
+    when(mockBlockValSet.isSingleValue()).thenReturn(true);
+    when(mockBlockValSet.getDoubleValuesSV()).thenReturn(new double[]{2, 10});
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    function.aggregateGroupBySV(2, new int[]{0, 0}, groupByResultHolder, 
blockValSetMap);
+    assertEquals(groupByResultHolder.getDoubleResult(0), 2.0);
+  }
+
   @DataProvider(name = "scenarios")
   Object[] scenarios() {
     return new Object[] {


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

Reply via email to