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

gortiz 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 29f91c6234d Add string type support to MIN/MAX aggregation functions 
(#16497)
29f91c6234d is described below

commit 29f91c6234d2fee30543496235efd17b47db457b
Author: Arunkumar Saravanan <[email protected]>
AuthorDate: Mon Aug 11 16:40:14 2025 +0530

    Add string type support to MIN/MAX aggregation functions (#16497)
---
 .../blocks/results/AggregationResultsBlock.java    |   3 +
 .../function/AggregationFunctionFactory.java       |   4 +
 .../function/AggregationFunctionUtils.java         |   2 +
 .../function/MaxStringAggregationFunction.java     | 164 +++++++++++
 .../function/MinStringAggregationFunction.java     | 163 +++++++++++
 .../function/AggregationFunctionFactoryTest.java   |  12 +
 .../function/MaxStringAggregationFunctionTest.java | 312 +++++++++++++++++++++
 .../function/MinStringAggregationFunctionTest.java | 306 ++++++++++++++++++++
 .../pinot/segment/spi/AggregationFunctionType.java |   2 +
 9 files changed, 968 insertions(+)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/AggregationResultsBlock.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/AggregationResultsBlock.java
index 58991924c28..91b4f14e3c2 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/AggregationResultsBlock.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/operator/blocks/results/AggregationResultsBlock.java
@@ -197,6 +197,9 @@ public class AggregationResultsBlock extends 
BaseResultsBlock {
       case DOUBLE:
         dataTableBuilder.setColumn(index, (double) result);
         break;
+      case STRING:
+        dataTableBuilder.setColumn(index, result.toString());
+        break;
       default:
         throw new IllegalStateException("Illegal column data type in 
intermediate result: " + columnDataType);
     }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactory.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactory.java
index 8685be9a1ed..d05316b5480 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactory.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactory.java
@@ -216,6 +216,10 @@ public class AggregationFunctionFactory {
             return new MinAggregationFunction(arguments, nullHandlingEnabled);
           case MAX:
             return new MaxAggregationFunction(arguments, nullHandlingEnabled);
+          case MINSTRING:
+            return new MinStringAggregationFunction(arguments, 
nullHandlingEnabled);
+          case MAXSTRING:
+            return new MaxStringAggregationFunction(arguments, 
nullHandlingEnabled);
           case SUM:
           case SUM0:
             return new SumAggregationFunction(arguments, nullHandlingEnabled);
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 5a74894116e..c9edf0bf00c 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
@@ -152,6 +152,8 @@ public class AggregationFunctionUtils {
         return dataTable.getLong(rowId, colId);
       case DOUBLE:
         return dataTable.getDouble(rowId, colId);
+      case STRING:
+        return dataTable.getString(rowId, colId);
       case OBJECT:
         CustomObject customObject = dataTable.getCustomObject(rowId, colId);
         return customObject != null ? 
aggregationFunction.deserializeIntermediateResult(customObject) : null;
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunction.java
new file mode 100644
index 00000000000..37eb822f213
--- /dev/null
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunction.java
@@ -0,0 +1,164 @@
+/**
+ * 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.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.core.common.BlockValSet;
+import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
+import org.apache.pinot.core.query.aggregation.ObjectAggregationResultHolder;
+import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
+import 
org.apache.pinot.core.query.aggregation.groupby.ObjectGroupByResultHolder;
+import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
+
+
+public class MaxStringAggregationFunction extends 
NullableSingleInputAggregationFunction<String, String> {
+
+  public MaxStringAggregationFunction(List<ExpressionContext> arguments, 
boolean nullHandlingEnabled) {
+    super(verifySingleArgument(arguments, "MAXSTRING"), nullHandlingEnabled);
+  }
+
+  @Override
+  public AggregationFunctionType getType() {
+    return AggregationFunctionType.MAXSTRING;
+  }
+
+  @Override
+  public AggregationResultHolder createAggregationResultHolder() {
+    return new ObjectAggregationResultHolder();
+  }
+
+  @Override
+  public GroupByResultHolder createGroupByResultHolder(int initialCapacity, 
int maxCapacity) {
+    return new ObjectGroupByResultHolder(initialCapacity, maxCapacity);
+  }
+
+  @Override
+  public void aggregate(int length, AggregationResultHolder 
aggregationResultHolder,
+      Map<ExpressionContext, BlockValSet> blockValSetMap) {
+    BlockValSet blockValSet = blockValSetMap.get(_expression);
+    if (blockValSet.getValueType().isNumeric()) {
+      throw new BadQueryRequestException("Cannot compute MAXSTRING for numeric 
column: "
+          + blockValSet.getValueType());
+    }
+    String[] stringValues = blockValSet.getStringValuesSV();
+    String maxValue = foldNotNull(length, blockValSet, null, (acum, from, to) 
-> {
+      String innerMax = stringValues[from];
+      for (int i = from + 1; i < to; i++) {
+        if (stringValues[i].compareTo(innerMax) > 0) {
+          innerMax = stringValues[i];
+        }
+      }
+      return acum == null ? innerMax : (acum.compareTo(innerMax) > 0 ? acum : 
innerMax);
+    });
+
+    String currentMax = aggregationResultHolder.getResult();
+    if (currentMax == null || (maxValue != null && 
maxValue.compareTo(currentMax) > 0)) {
+      aggregationResultHolder.setValue(maxValue);
+    }
+  }
+
+  @Override
+  public void aggregateGroupBySV(int length, int[] groupKeyArray, 
GroupByResultHolder groupByResultHolder,
+      Map<ExpressionContext, BlockValSet> blockValSetMap) {
+    BlockValSet blockValSet = blockValSetMap.get(_expression);
+    if (blockValSet.getValueType().isNumeric()) {
+      throw new BadQueryRequestException("Cannot compute MAXSTRING for numeric 
column: "
+          + blockValSet.getValueType());
+    }
+    String[] stringValues = blockValSet.getStringValuesSV();
+    forEachNotNull(length, blockValSet, (from, to) -> {
+      for (int i = from; i < to; i++) {
+        String value = stringValues[i];
+        int groupKey = groupKeyArray[i];
+        String currentMax = groupByResultHolder.getResult(groupKey);
+        if (currentMax == null || value.compareTo(currentMax) > 0) {
+          groupByResultHolder.setValueForKey(groupKey, value);
+        }
+      }
+    });
+  }
+
+  @Override
+  public void aggregateGroupByMV(int length, int[][] groupKeysArray, 
GroupByResultHolder groupByResultHolder,
+      Map<ExpressionContext, BlockValSet> blockValSetMap) {
+    BlockValSet blockValSet = blockValSetMap.get(_expression);
+    if (blockValSet.getValueType().isNumeric()) {
+      throw new BadQueryRequestException("Cannot compute MAXSTRING for numeric 
column: "
+          + blockValSet.getValueType());
+    }
+    String[] stringValues = blockValSet.getStringValuesSV();
+    forEachNotNull(length, blockValSet, (from, to) -> {
+      for (int i = from; i < to; i++) {
+        String value = stringValues[i];
+        for (int groupKey : groupKeysArray[i]) {
+          String currentMax = groupByResultHolder.getResult(groupKey);
+          if (currentMax == null || value.compareTo(currentMax) > 0) {
+            groupByResultHolder.setValueForKey(groupKey, value);
+          }
+        }
+      }
+    });
+  }
+
+  @Override
+  public String extractAggregationResult(AggregationResultHolder 
aggregationResultHolder) {
+    return aggregationResultHolder.getResult();
+  }
+
+  @Override
+  public String extractGroupByResult(GroupByResultHolder groupByResultHolder, 
int groupKey) {
+    return groupByResultHolder.getResult(groupKey);
+  }
+
+  @Override
+  public String merge(@Nullable String intermediateResult1, @Nullable String 
intermediateResult2) {
+    if (intermediateResult1 == null) {
+      return intermediateResult2;
+    }
+    if (intermediateResult2 == null) {
+      return intermediateResult1;
+    }
+    return intermediateResult1.compareTo(intermediateResult2) > 0 ? 
intermediateResult1 : intermediateResult2;
+  }
+
+  @Override
+  public ColumnDataType getIntermediateResultColumnType() {
+    return ColumnDataType.STRING;
+  }
+
+  @Override
+  public ColumnDataType getFinalResultColumnType() {
+    return ColumnDataType.STRING;
+  }
+
+  @Override
+  public String extractFinalResult(String intermediateResult) {
+    return intermediateResult;
+  }
+
+  @Override
+  public String mergeFinalResult(String finalResult1, String finalResult2) {
+    return merge(finalResult1, finalResult2);
+  }
+}
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunction.java
new file mode 100644
index 00000000000..bb12dd4b9e3
--- /dev/null
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunction.java
@@ -0,0 +1,163 @@
+/**
+ * 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.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.core.common.BlockValSet;
+import org.apache.pinot.core.query.aggregation.AggregationResultHolder;
+import org.apache.pinot.core.query.aggregation.ObjectAggregationResultHolder;
+import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
+import 
org.apache.pinot.core.query.aggregation.groupby.ObjectGroupByResultHolder;
+import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
+
+
+public class MinStringAggregationFunction extends 
NullableSingleInputAggregationFunction<String, String> {
+
+  public MinStringAggregationFunction(List<ExpressionContext> arguments, 
boolean nullHandlingEnabled) {
+    super(verifySingleArgument(arguments, "MINSTRING"), nullHandlingEnabled);
+  }
+
+  @Override
+  public AggregationFunctionType getType() {
+    return AggregationFunctionType.MINSTRING;
+  }
+
+  @Override
+  public AggregationResultHolder createAggregationResultHolder() {
+    return new ObjectAggregationResultHolder();
+  }
+
+  @Override
+  public GroupByResultHolder createGroupByResultHolder(int initialCapacity, 
int maxCapacity) {
+    return new ObjectGroupByResultHolder(initialCapacity, maxCapacity);
+  }
+
+  @Override
+  public void aggregate(int length, AggregationResultHolder 
aggregationResultHolder,
+      Map<ExpressionContext, BlockValSet> blockValSetMap) {
+    BlockValSet blockValSet = blockValSetMap.get(_expression);
+    if (blockValSet.getValueType().isNumeric()) {
+      throw new BadQueryRequestException("Cannot compute MINSTRING for numeric 
column: "
+          + blockValSet.getValueType());
+    }
+    String[] stringValues = blockValSet.getStringValuesSV();
+    String minValue = foldNotNull(length, blockValSet, null, (acum, from, to) 
-> {
+      String innerMin = stringValues[from];
+      for (int i = from + 1; i < to; i++) {
+        if (stringValues[i].compareTo(innerMin) < 0) {
+          innerMin = stringValues[i];
+        }
+      }
+      return acum == null ? innerMin : (acum.compareTo(innerMin) < 0 ? acum : 
innerMin);
+    });
+    String currentMin = aggregationResultHolder.getResult();
+    if (currentMin == null || (minValue != null && 
minValue.compareTo(currentMin) < 0)) {
+      aggregationResultHolder.setValue(minValue);
+    }
+  }
+
+  @Override
+  public void aggregateGroupBySV(int length, int[] groupKeyArray, 
GroupByResultHolder groupByResultHolder,
+      Map<ExpressionContext, BlockValSet> blockValSetMap) {
+    BlockValSet blockValSet = blockValSetMap.get(_expression);
+    if (blockValSet.getValueType().isNumeric()) {
+      throw new BadQueryRequestException("Cannot compute MINSTRING for numeric 
column: "
+          + blockValSet.getValueType());
+    }
+    String[] stringValues = blockValSet.getStringValuesSV();
+    forEachNotNull(length, blockValSet, (from, to) -> {
+      for (int i = from; i < to; i++) {
+        String value = stringValues[i];
+        int groupKey = groupKeyArray[i];
+        String currentMin = groupByResultHolder.getResult(groupKey);
+        if (currentMin == null || value.compareTo(currentMin) < 0) {
+          groupByResultHolder.setValueForKey(groupKey, value);
+        }
+      }
+    });
+  }
+
+  @Override
+  public void aggregateGroupByMV(int length, int[][] groupKeysArray, 
GroupByResultHolder groupByResultHolder,
+      Map<ExpressionContext, BlockValSet> blockValSetMap) {
+    BlockValSet blockValSet = blockValSetMap.get(_expression);
+    if (blockValSet.getValueType().isNumeric()) {
+      throw new BadQueryRequestException("Cannot compute MINSTRING for numeric 
column: "
+          + blockValSet.getValueType());
+    }
+    String[] stringValues = blockValSet.getStringValuesSV();
+    forEachNotNull(length, blockValSet, (from, to) -> {
+      for (int i = from; i < to; i++) {
+        String value = stringValues[i];
+        for (int groupKey : groupKeysArray[i]) {
+          String currentMin = groupByResultHolder.getResult(groupKey);
+          if (currentMin == null || value.compareTo(currentMin) < 0) {
+            groupByResultHolder.setValueForKey(groupKey, value);
+          }
+        }
+      }
+    });
+  }
+
+  @Override
+  public String extractAggregationResult(AggregationResultHolder 
aggregationResultHolder) {
+    return aggregationResultHolder.getResult();
+  }
+
+  @Override
+  public String extractGroupByResult(GroupByResultHolder groupByResultHolder, 
int groupKey) {
+    return groupByResultHolder.getResult(groupKey);
+  }
+
+  @Override
+  public String merge(@Nullable String intermediateResult1, @Nullable String 
intermediateResult2) {
+    if (intermediateResult1 == null) {
+      return intermediateResult2;
+    }
+    if (intermediateResult2 == null) {
+      return intermediateResult1;
+    }
+    return intermediateResult1.compareTo(intermediateResult2) < 0 ? 
intermediateResult1 : intermediateResult2;
+  }
+
+  @Override
+  public ColumnDataType getIntermediateResultColumnType() {
+    return ColumnDataType.STRING;
+  }
+
+  @Override
+  public ColumnDataType getFinalResultColumnType() {
+    return ColumnDataType.STRING;
+  }
+
+  @Override
+  public String extractFinalResult(String intermediateResult) {
+    return intermediateResult;
+  }
+
+  @Override
+  public String mergeFinalResult(String finalResult1, String finalResult2) {
+    return merge(finalResult1, finalResult2);
+  }
+}
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactoryTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactoryTest.java
index 0caf40536bc..2ec20535f2b 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactoryTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/AggregationFunctionFactoryTest.java
@@ -52,6 +52,18 @@ public class AggregationFunctionFactoryTest {
     assertEquals(aggregationFunction.getType(), AggregationFunctionType.MAX);
     assertEquals(aggregationFunction.getResultColumnName(), 
function.toString());
 
+    function = getFunction("MiNsTrInG");
+    aggregationFunction = 
AggregationFunctionFactory.getAggregationFunction(function, false);
+    assertTrue(aggregationFunction instanceof MinStringAggregationFunction);
+    assertEquals(aggregationFunction.getType(), 
AggregationFunctionType.MINSTRING);
+    assertEquals(aggregationFunction.getResultColumnName(), 
function.toString());
+
+    function = getFunction("MaXsTrInG");
+    aggregationFunction = 
AggregationFunctionFactory.getAggregationFunction(function, false);
+    assertTrue(aggregationFunction instanceof MaxStringAggregationFunction);
+    assertEquals(aggregationFunction.getType(), 
AggregationFunctionType.MAXSTRING);
+    assertEquals(aggregationFunction.getResultColumnName(), 
function.toString());
+
     function = getFunction("SuM");
     aggregationFunction = 
AggregationFunctionFactory.getAggregationFunction(function, false);
     assertTrue(aggregationFunction instanceof SumAggregationFunction);
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunctionTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunctionTest.java
new file mode 100644
index 00000000000..d0cf29d825c
--- /dev/null
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MaxStringAggregationFunctionTest.java
@@ -0,0 +1,312 @@
+/**
+ * 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.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import java.util.Collections;
+import java.util.HashMap;
+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.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+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.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+
+public class MaxStringAggregationFunctionTest extends 
AbstractAggregationFunctionTest {
+
+  /**
+   * Helper method to create a FluentQueryTest builder for a table with a 
single String field.
+   * This is used to simulate the DataTypeScenario concept from numeric 
aggregation tests,
+   * but fixed for the STRING data type.
+   */
+  protected FluentQueryTest.DeclaringTable getDeclaringTable(boolean 
enableColumnBasedNullHandling) {
+    return FluentQueryTest.withBaseDir(_baseDir)
+        .givenTable(
+            new Schema.SchemaBuilder()
+                .setSchemaName("testTable")
+                
.setEnableColumnBasedNullHandling(enableColumnBasedNullHandling)
+                .addSingleValueDimension("myField", FieldSpec.DataType.STRING)
+                .build(), SINGLE_FIELD_TABLE_CONFIG);
+  }
+
+  @Test
+  public void testNumericColumnExceptioninAggregateMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MaxStringAggregationFunction function = new 
MaxStringAggregationFunction(Collections.singletonList(expression),
+        false);
+
+    AggregationResultHolder resultHolder = 
function.createAggregationResultHolder();
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    try {
+      function.aggregate(10, resultHolder, blockValSetMap);
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("Cannot compute MAXSTRING for numeric 
column"));
+    }
+  }
+
+  @Test
+  public void testNumericColumnExceptioninAggregateGroupBySVMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MaxStringAggregationFunction function = new 
MaxStringAggregationFunction(Collections.singletonList(expression),
+        false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+    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 MAXSTRING for numeric 
column"));
+    }
+  }
+
+  @Test
+  public void testNumericColumnExceptioninAggregateGroupByMVMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MaxStringAggregationFunction function = new 
MaxStringAggregationFunction(Collections.singletonList(expression),
+        false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+    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 MAXSTRING for numeric 
column"));
+    }
+  }
+
+  @Test
+  public void testFunctionBasics() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MaxStringAggregationFunction function = new 
MaxStringAggregationFunction(Collections.singletonList(expression),
+        false);
+
+    // Test function type
+    assertEquals(function.getType(), AggregationFunctionType.MAXSTRING);
+
+    // Test string comparisons
+    assertEquals(function.merge("apple", "banana"), "banana");
+    assertEquals(function.merge("banana", "apple"), "banana");
+    assertEquals(function.merge("", "apple"), "apple");
+    assertEquals(function.merge("apple", ""), "apple");
+
+    // Test null handling
+    assertEquals(function.merge("apple", null), "apple");
+    assertEquals(function.merge(null, "apple"), "apple");
+    assertNull(function.merge(null, null));
+    assertEquals(function.merge("apple", "null"), "null");
+
+    // Test final result merging
+    assertEquals(function.mergeFinalResult("apple", "banana"), "banana");
+  }
+
+  @Test
+  void aggregationAllNullsWithNullHandlingDisabled() {
+    // For MAXSTRING, when null handling is disabled, and all values are null,
+    // the result should be 'null' as there's no valid string to compare.
+    // This differs from numeric MAX/MIN which might return an initial default 
value.
+    getDeclaringTable(false) // nullHandlingEnabled = false
+        .onFirstInstance("myField",
+            "null",
+            "null"
+        ).andOnSecondInstance("myField",
+            "null"
+        ).whenQuery("select maxstring(myField) from testTable")
+        .thenResultIs("STRING", "\"null\""); // Asserting "null" as a string 
literal for the result
+  }
+
+  @Test
+  void aggregationAllNullsWithNullHandlingEnabled() {
+    // When null handling is enabled, and all values are null, the result 
should also be 'null'.
+    getDeclaringTable(true) // nullHandlingEnabled = true
+        .onFirstInstance("myField",
+            "null",
+            "null"
+        ).andOnSecondInstance("myField",
+            "null"
+        ).whenQuery("select maxstring(myField) from testTable")
+        .thenResultIs("STRING", "\"null\""); // Asserting "null" as a string 
literal for the result
+  }
+
+  @Test
+  void aggregationGroupBySVAllNullsWithNullHandlingDisabled() {
+    // For group by, if all values in a group are null and null handling is 
disabled,
+    // the group's result for MAXSTRING should be 'null'.
+    getDeclaringTable(false) // nullHandlingEnabled = false
+        .onFirstInstance("myField",
+            "null",
+            "null"
+        ).andOnSecondInstance("myField",
+            "null"
+        ).whenQuery("select 'literal', maxstring(myField) from testTable group 
by 'literal'")
+        // Expected "null" as a string literal for the aggregated column
+        .thenResultIs("STRING | STRING", "literal | \"null\"");
+  }
+
+  @Test
+  void aggregationGroupBySVAllNullsWithNullHandlingEnabled() {
+    // For group by, if all values in a group are null and null handling is 
enabled,
+    // the group's result for MAXSTRING should be 'null'.
+    getDeclaringTable(true) // nullHandlingEnabled = true
+        .onFirstInstance("myField",
+            "null",
+            "null"
+        ).andOnSecondInstance("myField",
+            "null"
+        ).whenQuery("select 'literal', maxstring(myField) from testTable group 
by 'literal'")
+        .thenResultIs("STRING | STRING", "literal | \"null\"");
+  }
+
+  @Test
+  void aggregationWithNullHandlingDisabled() {
+    // With null handling disabled, null values are effectively skipped, and 
the maximum non-null
+    // string should be found. The updated function handles this correctly.
+    getDeclaringTable(false) // nullHandlingEnabled = false
+        .onFirstInstance("myField",
+            "cat",
+            "null",
+            "apple"
+        ).andOnSecondInstance("myField",
+            "null",
+            "zebra",
+            "null"
+        ).whenQuery("select maxstring(myField) from testTable")
+        .thenResultIs("STRING", "zebra"); // Max of {"cat", "apple", "zebra"} 
is "zebra"
+  }
+
+  @Test
+  void aggregationWithNullHandlingEnabled() {
+    // With null handling enabled, null values are explicitly ignored, and the 
maximum non-null
+    // string should be found. The updated function handles this correctly.
+    getDeclaringTable(true) // nullHandlingEnabled = true
+        .onFirstInstance("myField",
+            "cat",
+            "null",
+            "apple"
+        ).andOnSecondInstance("myField",
+            "null",
+            "zebra",
+            "null"
+        ).whenQuery("select maxstring(myField) from testTable")
+        .thenResultIs("STRING", "zebra"); // Max of {"cat", "apple", "zebra"} 
is "zebra"
+  }
+
+  @Test
+  void aggregationGroupBySVWithNullHandlingDisabled() {
+    // Group By on a single value (SV) column with mixed nulls and non-nulls.
+    // Null handling disabled: nulls are ignored if there's at least one 
non-null value in the group.
+    // The updated function should now correctly find the max among non-nulls.
+    getDeclaringTable(false) // nullHandlingEnabled = false
+        .onFirstInstance("myField",
+            "alpha", // Grouped with 'literal'
+            "null",  // Grouped with 'literal'
+            "gamma"  // Grouped with 'literal'
+        ).andOnSecondInstance("myField",
+            "null",  // Grouped with 'literal'
+            "beta",  // Grouped with 'literal'
+            "null"   // Grouped with 'literal'
+        ).whenQuery("select 'literal', maxstring(myField) from testTable group 
by 'literal'")
+        .thenResultIs("STRING | STRING",
+            "literal | \"null\""); // Max of {"alpha", null, "gamma", "beta"} 
is "null" when null handling is disabled
+  }
+
+  @Test
+  void aggregationGroupBySVWithNullHandlingEnabled() {
+    // Group By on a single value (SV) column with mixed nulls and non-nulls.
+    // Null handling enabled: nulls are ignored.
+    // The updated function should now correctly find the max among non-nulls.
+    getDeclaringTable(true) // nullHandlingEnabled = true
+        .onFirstInstance("myField",
+            "alpha", // Grouped with 'literal'
+            "null",  // Grouped with 'literal'
+            "gamma"  // Grouped with 'literal'
+        ).andOnSecondInstance("myField",
+            "null",  // Grouped with 'literal'
+            "beta",  // Grouped with 'literal'
+            "null"   // Grouped with 'literal'
+        ).whenQueryWithNullHandlingEnabled("select 'literal', 
maxstring(myField) from testTable group by 'literal'")
+        .thenResultIs("STRING | STRING", "literal | gamma"); // Max of 
{"alpha", "gamma", "beta"} is "gamma"
+  }
+
+  @Test
+  void aggregationGroupByMV() {
+    FluentQueryTest.withBaseDir(_baseDir)
+        .givenTable(
+            new Schema.SchemaBuilder()
+                .setSchemaName("testTable")
+                .setEnableColumnBasedNullHandling(true) // Set at schema level 
for general behavior
+                .addMultiValueDimension("tags", FieldSpec.DataType.STRING) // 
Dimension for tags
+                .addDimensionField("value", FieldSpec.DataType.STRING)
+                .build(), SINGLE_FIELD_TABLE_CONFIG)
+        .onFirstInstance(
+            new Object[]{"tag1;tag2", "banana"}, // Row 1: tag1 -> "banana", 
tag2 -> "banana"
+            new Object[]{"tag2;tag3", null}      // Row 2: tag2 -> null, tag3 
-> null
+        )
+        .andOnSecondInstance(
+            new Object[]{"tag1;tag2", "apple"},   // Row 3: tag1 -> "apple", 
tag2 -> "apple"
+            new Object[]{"tag2;tag3", "cherry"}  // Row 4: tag2 -> "cherry", 
tag3 -> "cherry"
+        )
+        // Query without explicit null handling enabled via query option (uses 
table schema setting or default)
+        .whenQuery("select tags, MAXSTRING(value) from testTable group by tags 
order by tags")
+        .thenResultIs(
+            "STRING | STRING",
+            "tag1    | banana", // Values for tag1: "banana", "apple". Max is 
"banana".
+            "tag2    | \"null\"",
+            // Values for tag2: "banana", "apple", null, "cherry". Max is 
"null" (nulls ignored). This is because
+            // when null handling is disabled, the null value is read as 
"null" and we need to honor that
+            "tag3    | \"null\""  // Values for tag3: null, "cherry". Max is 
"null".
+        )
+        // Query with explicit null handling enabled via query option
+        .whenQueryWithNullHandlingEnabled("select tags, MAXSTRING(value) from 
testTable "
+            + "group by tags order by tags")
+        .thenResultIs(
+            "STRING | STRING",
+            "tag1    | banana", // Values for tag1: "banana", "apple". Max is 
"banana".
+            "tag2    | cherry", // Values for tag2: "banana", "apple", 
"cherry". Max is "cherry".
+            "tag3    | cherry"  // Values for tag3: "cherry". Max is "cherry".
+        );
+  }
+}
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunctionTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunctionTest.java
new file mode 100644
index 00000000000..89a3bd17d00
--- /dev/null
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/aggregation/function/MinStringAggregationFunctionTest.java
@@ -0,0 +1,306 @@
+/**
+ * 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.
+ */
+package org.apache.pinot.core.query.aggregation.function;
+
+import java.util.Collections;
+import java.util.HashMap;
+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.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+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.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+
+public class MinStringAggregationFunctionTest extends 
AbstractAggregationFunctionTest {
+
+  /**
+   * Helper method to create a FluentQueryTest builder for a table with a 
single String field.
+   * This is used to simulate the DataTypeScenario concept from numeric 
aggregation tests,
+   * but fixed for the STRING data type.
+   */
+  protected FluentQueryTest.DeclaringTable getDeclaringTable(boolean 
enableColumnBasedNullHandling) {
+    return FluentQueryTest.withBaseDir(_baseDir)
+        .givenTable(
+            new Schema.SchemaBuilder()
+                .setSchemaName("testTable")
+                
.setEnableColumnBasedNullHandling(enableColumnBasedNullHandling)
+                .addSingleValueDimension("myField", FieldSpec.DataType.STRING)
+                .build(), SINGLE_FIELD_TABLE_CONFIG);
+  }
+
+  @Test
+  public void testNumericColumnExceptioninAggregateMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MinStringAggregationFunction function = new 
MinStringAggregationFunction(Collections.singletonList(expression),
+        false);
+
+    AggregationResultHolder resultHolder = 
function.createAggregationResultHolder();
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+    blockValSetMap.put(expression, mockBlockValSet);
+
+    try {
+      function.aggregate(10, resultHolder, blockValSetMap);
+      fail("Should throw BadQueryRequestException");
+    } catch (BadQueryRequestException e) {
+      assertTrue(e.getMessage().contains("Cannot compute MINSTRING for numeric 
column"));
+    }
+  }
+
+  @Test
+  public void testNumericColumnExceptioninAggregateGroupBySVMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MinStringAggregationFunction function = new 
MinStringAggregationFunction(Collections.singletonList(expression),
+        false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+    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 MINSTRING for numeric 
column"));
+    }
+  }
+
+  @Test
+  public void testNumericColumnExceptioninAggregateGroupByMVMethod() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MinStringAggregationFunction function = new 
MinStringAggregationFunction(Collections.singletonList(expression),
+        false);
+
+    GroupByResultHolder groupByResultHolder = 
function.createGroupByResultHolder(10, 20);
+    Map<ExpressionContext, BlockValSet> blockValSetMap = new HashMap<>();
+    BlockValSet mockBlockValSet = mock(BlockValSet.class);
+    when(mockBlockValSet.getValueType()).thenReturn(FieldSpec.DataType.INT);
+    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 MINSTRING for numeric 
column"));
+    }
+  }
+
+  @Test
+  public void testFunctionBasics() {
+    ExpressionContext expression = RequestContextUtils.getExpression("column");
+    MinStringAggregationFunction function = new 
MinStringAggregationFunction(Collections.singletonList(expression),
+        false);
+
+    // Test function type
+    assertEquals(function.getType(), AggregationFunctionType.MINSTRING);
+
+    // Test string comparisons
+    assertEquals(function.merge("apple", "banana"), "apple");
+    assertEquals(function.merge("banana", "apple"), "apple");
+    assertEquals(function.merge("", "apple"), "");
+    assertEquals(function.merge("apple", ""), "");
+
+    // Test null handling
+    assertEquals(function.merge("apple", null), "apple");
+    assertEquals(function.merge(null, "apple"), "apple");
+    assertNull(function.merge(null, null));
+
+    // Test final result merging
+    assertEquals(function.mergeFinalResult("apple", "banana"), "apple");
+  }
+
+  @Test
+  void aggregationAllNullsWithNullHandlingDisabled() {
+    // For MINSTRING, when null handling is disabled, and all values are null,
+    // the result should be 'null' as there's no valid string to compare.
+    // This differs from numeric MAX/MIN which might return an initial default 
value.
+    getDeclaringTable(false) // nullHandlingEnabled = false
+        .onFirstInstance("myField",
+            "null",
+            "null"
+        ).andOnSecondInstance("myField",
+            "null"
+        ).whenQuery("select minstring(myField) from testTable")
+        .thenResultIs("STRING", "\"null\""); // Asserting "null" as a string 
literal for the result
+  }
+
+  @Test
+  void aggregationAllNullsWithNullHandlingEnabled() {
+    // When null handling is enabled, and all values are null, the result 
should also be 'null'.
+    getDeclaringTable(true) // nullHandlingEnabled = true
+        .onFirstInstance("myField",
+            "null",
+            "null"
+        ).andOnSecondInstance("myField",
+            "null"
+        ).whenQuery("select minstring(myField) from testTable")
+        .thenResultIs("STRING", "\"null\""); // Asserting "null" as a string 
literal for the result
+  }
+
+  @Test
+  void aggregationGroupBySVAllNullsWithNullHandlingDisabled() {
+    // For group by, if all values in a group are null and null handling is 
disabled,
+    // the group's result for MINSTRING should be 'null'.
+    getDeclaringTable(false) // nullHandlingEnabled = false
+        .onFirstInstance("myField",
+            "null",
+            "null"
+        ).andOnSecondInstance("myField",
+            "null"
+        ).whenQuery("select 'literal', minstring(myField) from testTable group 
by 'literal'")
+        // Expected "null" as a string literal for the aggregated column
+        .thenResultIs("STRING | STRING", "literal | \"null\"");
+  }
+
+  @Test
+  void aggregationGroupBySVAllNullsWithNullHandlingEnabled() {
+    // For group by, if all values in a group are null and null handling is 
enabled,
+    // the group's result for MINSTRING should be 'null'.
+    getDeclaringTable(true) // nullHandlingEnabled = true
+        .onFirstInstance("myField",
+            "null",
+            "null"
+        ).andOnSecondInstance("myField",
+            "null"
+        ).whenQuery("select 'literal', minstring(myField) from testTable group 
by 'literal'")
+        .thenResultIs("STRING | STRING", "literal | \"null\"");
+  }
+
+  @Test
+  void aggregationWithNullHandlingDisabled() {
+    // With null handling disabled, null values are effectively skipped, and 
the minimum non-null
+    // string should be found. The updated function handles this correctly.
+    getDeclaringTable(false) // nullHandlingEnabled = false
+        .onFirstInstance("myField",
+            "cat",
+            "null",
+            "apple"
+        ).andOnSecondInstance("myField",
+            "null",
+            "zebra",
+            "null"
+        ).whenQuery("select minstring(myField) from testTable")
+        .thenResultIs("STRING", "apple"); // Min of {"cat", "apple", "zebra"} 
is "apple"
+  }
+
+  @Test
+  void aggregationWithNullHandlingEnabled() {
+    // With null handling enabled, null values are explicitly ignored, and the 
minimum non-null
+    // string should be found. The updated function handles this correctly.
+    getDeclaringTable(true) // nullHandlingEnabled = true
+        .onFirstInstance("myField",
+            "cat",
+            "null",
+            "apple"
+        ).andOnSecondInstance("myField",
+            "null",
+            "zebra",
+            "null"
+        ).whenQuery("select minstring(myField) from testTable")
+        .thenResultIs("STRING", "apple"); // Min of {"cat", "apple", "zebra"} 
is "apple"
+  }
+
+  @Test
+  void aggregationGroupBySVWithNullHandlingDisabled() {
+    // Group By on a single value (SV) column with mixed nulls and non-nulls.
+    // Null handling disabled: nulls are ignored if there's at least one 
non-null value in the group.
+    // The updated function should now correctly find the min among non-nulls.
+    getDeclaringTable(false) // nullHandlingEnabled = false
+        .onFirstInstance("myField",
+            "alpha", // Grouped with 'literal'
+            "null",  // Grouped with 'literal'
+            "gamma"  // Grouped with 'literal'
+        ).andOnSecondInstance("myField",
+            "null",  // Grouped with 'literal'
+            "beta",  // Grouped with 'literal'
+            "null"   // Grouped with 'literal'
+        ).whenQuery("select 'literal', minstring(myField) from testTable group 
by 'literal'")
+        .thenResultIs("STRING | STRING", "literal | alpha"); // Min of 
{"alpha", "gamma", "beta"} is "alpha"
+  }
+
+  @Test
+  void aggregationGroupBySVWithNullHandlingEnabled() {
+    // Group By on a single value (SV) column with mixed nulls and non-nulls.
+    // Null handling enabled: nulls are ignored.
+    // The updated function should now correctly find the min among non-nulls.
+    getDeclaringTable(true) // nullHandlingEnabled = true
+        .onFirstInstance("myField",
+            "alpha", // Grouped with 'literal'
+            "null",  // Grouped with 'literal'
+            "gamma"  // Grouped with 'literal'
+        ).andOnSecondInstance("myField",
+            "null",  // Grouped with 'literal'
+            "beta",  // Grouped with 'literal'
+            "null"   // Grouped with 'literal'
+        ).whenQuery("select 'literal', minstring(myField) from testTable group 
by 'literal'")
+        .thenResultIs("STRING | STRING", "literal | alpha"); // Min of 
{"alpha", "gamma", "beta"} is "alpha"
+  }
+
+  @Test
+  void aggregationGroupByMV() {
+    FluentQueryTest.withBaseDir(_baseDir)
+        .givenTable(
+            new Schema.SchemaBuilder()
+                .setSchemaName("testTable")
+                .setEnableColumnBasedNullHandling(true) // Set at schema level 
for general behavior
+                .addMultiValueDimension("tags", FieldSpec.DataType.STRING) // 
Dimension for tags
+                .addDimensionField("value", FieldSpec.DataType.STRING)
+                .build(), SINGLE_FIELD_TABLE_CONFIG)
+        .onFirstInstance(
+            new Object[]{"tag1;tag2", "banana"}, // Row 1: tag1 -> "banana", 
tag2 -> "banana"
+            new Object[]{"tag2;tag3", null}      // Row 2: tag2 -> null, tag3 
-> null
+        )
+        .andOnSecondInstance(
+            new Object[]{"tag1;tag2", "apple"},   // Row 3: tag1 -> "apple", 
tag2 -> "apple"
+            new Object[]{"tag2;tag3", "cherry"}  // Row 4: tag2 -> "cherry", 
tag3 -> "cherry"
+        )
+        // Query without explicit null handling enabled via query option (uses 
table schema setting or default)
+        .whenQuery("select tags, MINSTRING(value) from testTable group by tags 
order by tags")
+        .thenResultIs(
+            "STRING | STRING",
+            "tag1    | apple", // Values for tag1: "banana", "apple". Min is 
"apple".
+            "tag2    | apple", // Values for tag2: "banana", "apple", 
"cherry". Min is "apple".
+            "tag3    | cherry"  // Values for tag3: null, "cherry". Min is 
"cherry".
+        )
+        // Query with explicit null handling enabled via query option
+        .whenQueryWithNullHandlingEnabled("select tags, MINSTRING(value) from 
testTable "
+            + "group by tags order by tags")
+        .thenResultIs(
+            "STRING | STRING",
+            "tag1    | apple", // Values for tag1: "banana", "apple". Min is 
"apple".
+            "tag2    | apple", // Values for tag2: "banana", "apple", 
"cherry". Min is "apple".
+            "tag3    | cherry"  // Values for tag3: null, "cherry". Min is 
"cherry".
+        );
+  }
+}
diff --git 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/AggregationFunctionType.java
 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/AggregationFunctionType.java
index cace39ff1a1..ede51582739 100644
--- 
a/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/AggregationFunctionType.java
+++ 
b/pinot-segment-spi/src/main/java/org/apache/pinot/segment/spi/AggregationFunctionType.java
@@ -53,6 +53,8 @@ public enum AggregationFunctionType {
   // TODO: min/max only supports NUMERIC in Pinot, where Calcite supports 
COMPARABLE_ORDERED
   MIN("min", SqlTypeName.DOUBLE, SqlTypeName.DOUBLE),
   MAX("max", SqlTypeName.DOUBLE, SqlTypeName.DOUBLE),
+  MINSTRING("minString", SqlTypeName.VARCHAR, SqlTypeName.VARCHAR),
+  MAXSTRING("maxString", SqlTypeName.VARCHAR, SqlTypeName.VARCHAR),
   SUM("sum", SqlTypeName.DOUBLE, SqlTypeName.DOUBLE),
   SUM0("$sum0", SqlTypeName.DOUBLE, SqlTypeName.DOUBLE),
   SUMPRECISION("sumPrecision", ReturnTypes.explicit(SqlTypeName.DECIMAL), 
OperandTypes.ANY, SqlTypeName.OTHER),


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

Reply via email to