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

xiangfu0 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 dc759c51e66 Support FIRST_WITH_TIME and LAST_WITH_TIME without a data 
type (#19986)
dc759c51e66 is described below

commit dc759c51e667286ae9f9fcb0f6e4dc9ddbe34b1f
Author: Xiang Fu <[email protected]>
AuthorDate: Fri Oct 23 16:28:27 2026 -0700

    Support FIRST_WITH_TIME and LAST_WITH_TIME without a data type (#19986)
    
    Both query engines rewrite FIRST_WITH_TIME(dataColumn, timeColumn) and 
LAST_WITH_TIME(dataColumn, timeColumn) to the explicit form with the data 
column's type, and the explicit form now supports every single-value data type 
(adding TIMESTAMP, BIG_DECIMAL, BYTES, UUID and JSON).
---
 .../apache/pinot/core/common/ObjectSerDeUtils.java |  47 +++++-
 .../function/AggregationFunctionUtils.java         |   3 +
 ...igDecimalValueWithTimeAggregationFunction.java} |  50 ++++---
 ...irstBytesValueWithTimeAggregationFunction.java} |  67 +++++----
 ...rstLastWithTimeAggregationFunctionProvider.java |  25 ++++
 .../FirstLongValueWithTimeAggregationFunction.java |  16 +-
 .../function/FirstWithTimeAggregationFunction.java |  12 +-
 ...igDecimalValueWithTimeAggregationFunction.java} |  50 ++++---
 ...LastBytesValueWithTimeAggregationFunction.java} |  67 +++++----
 .../LastLongValueWithTimeAggregationFunction.java  |  16 +-
 .../function/LastWithTimeAggregationFunction.java  |  12 +-
 .../AggregateFunctionRewriteOptimizer.java         | 109 +++++++++-----
 .../pinot/core/common/ObjectSerDeUtilsTest.java    |  34 +++++
 .../function/AggregationFunctionUtilsTest.java     |  25 +++-
 .../AggregateFunctionRewriteOptimizerTest.java     | 147 ++++++++++++++++++
 .../pinot/queries/FirstWithTimeQueriesTest.java    | 148 +++++++++++++++++++
 .../pinot/queries/LastWithTimeQueriesTest.java     | 148 +++++++++++++++++++
 .../PinotAggregateTypeArgumentRewriteRule.java     | 164 +++++++++++++++++++++
 .../org/apache/pinot/query/QueryEnvironment.java   |   3 +
 .../apache/pinot/query/QueryCompilationTest.java   |  11 ++
 .../pinot/query/QueryPlannerRuleOptionsTest.java   |  10 ++
 .../src/test/resources/queries/AggregatePlans.json |  39 +++++
 .../query/runtime/queries/QueryRunnerTestBase.java |   9 ++
 .../src/test/resources/queries/UDFAggregates.json  |  76 +++++++++-
 .../local/customobject/BigDecimalLongPair.java     |  52 +++++++
 .../segment/local/customobject/BytesLongPair.java  |  51 +++++++
 .../pinot/segment/spi/AggregationFunctionType.java |  10 +-
 27 files changed, 1239 insertions(+), 162 deletions(-)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/common/ObjectSerDeUtils.java 
b/pinot-core/src/main/java/org/apache/pinot/core/common/ObjectSerDeUtils.java
index b3c17dc809c..4259bd3873b 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/common/ObjectSerDeUtils.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/common/ObjectSerDeUtils.java
@@ -85,6 +85,8 @@ import 
org.apache.pinot.core.query.aggregation.utils.exprminmax.ExprMinMaxObject
 import org.apache.pinot.core.query.utils.idset.IdSet;
 import org.apache.pinot.core.query.utils.idset.IdSets;
 import org.apache.pinot.segment.local.customobject.AvgPair;
+import org.apache.pinot.segment.local.customobject.BigDecimalLongPair;
+import org.apache.pinot.segment.local.customobject.BytesLongPair;
 import org.apache.pinot.segment.local.customobject.CovarianceTuple;
 import org.apache.pinot.segment.local.customobject.CpcSketchAccumulator;
 import org.apache.pinot.segment.local.customobject.DoubleLongPair;
@@ -173,7 +175,9 @@ public class ObjectSerDeUtils {
     FunnelStepEventAccumulator(51),
     BigDecimalArrayList(52),
     BigDecimalSet(53),
-    BytesArrayList(54);
+    BytesArrayList(54),
+    BigDecimalLongPair(55),
+    BytesLongPair(56);
 
     private final int _value;
 
@@ -287,6 +291,10 @@ public class ObjectSerDeUtils {
         return ObjectType.DoubleLongPair;
       } else if (value instanceof StringLongPair) {
         return ObjectType.StringLongPair;
+      } else if (value instanceof BigDecimalLongPair) {
+        return ObjectType.BigDecimalLongPair;
+      } else if (value instanceof BytesLongPair) {
+        return ObjectType.BytesLongPair;
       } else if (value instanceof CovarianceTuple) {
         return ObjectType.CovarianceTuple;
       } else if (value instanceof VarianceTuple) {
@@ -810,6 +818,41 @@ public class ObjectSerDeUtils {
       return StringLongPair.fromByteBuffer(byteBuffer);
     }
   };
+  public static final ObjectSerDe<BigDecimalLongPair> 
BIG_DECIMAL_LONG_PAIR_SER_DE =
+      new ObjectSerDe<BigDecimalLongPair>() {
+
+        @Override
+        public byte[] serialize(BigDecimalLongPair bigDecimalLongPair) {
+          return bigDecimalLongPair.toBytes();
+        }
+
+        @Override
+        public BigDecimalLongPair deserialize(byte[] bytes) {
+          return BigDecimalLongPair.fromBytes(bytes);
+        }
+
+        @Override
+        public BigDecimalLongPair deserialize(ByteBuffer byteBuffer) {
+          return BigDecimalLongPair.fromByteBuffer(byteBuffer);
+        }
+      };
+  public static final ObjectSerDe<BytesLongPair> BYTES_LONG_PAIR_SER_DE = new 
ObjectSerDe<BytesLongPair>() {
+
+    @Override
+    public byte[] serialize(BytesLongPair bytesLongPair) {
+      return bytesLongPair.toBytes();
+    }
+
+    @Override
+    public BytesLongPair deserialize(byte[] bytes) {
+      return BytesLongPair.fromBytes(bytes);
+    }
+
+    @Override
+    public BytesLongPair deserialize(ByteBuffer byteBuffer) {
+      return BytesLongPair.fromByteBuffer(byteBuffer);
+    }
+  };
 
   public static final ObjectSerDe<CovarianceTuple> 
COVARIANCE_TUPLE_OBJECT_SER_DE = new ObjectSerDe<CovarianceTuple>() {
     @Override
@@ -2089,6 +2132,8 @@ public class ObjectSerDeUtils {
       BIG_DECIMAL_ARRAY_LIST_SER_DE,
       BIG_DECIMAL_SET_SER_DE,
       BYTES_ARRAY_LIST_SER_DE,
+      BIG_DECIMAL_LONG_PAIR_SER_DE,
+      BYTES_LONG_PAIR_SER_DE,
   };
   //@formatter:on
 
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 649ab7ad9c2..13c93f4023d 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
@@ -77,6 +77,7 @@ 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;
+import org.apache.pinot.spi.utils.UuidUtils;
 
 
 /// The `AggregationFunctionUtils` class provides utility methods for 
aggregation function.
@@ -355,6 +356,8 @@ public class AggregationFunctionUtils {
         return dataTable.getString(rowId, colId);
       case BYTES:
         return dataTable.getBytes(rowId, colId).getBytes();
+      case UUID:
+        return UuidUtils.toUUID(dataTable.getBytes(rowId, colId));
       case INT_ARRAY:
         return dataTable.getIntArray(rowId, colId);
       case LONG_ARRAY:
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstBigDecimalValueWithTimeAggregationFunction.java
similarity index 64%
copy from 
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
copy to 
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstBigDecimalValueWithTimeAggregationFunction.java
index 382abe1be19..155779960ad 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstBigDecimalValueWithTimeAggregationFunction.java
@@ -18,57 +18,59 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import java.math.BigDecimal;
 import org.apache.pinot.common.CustomObject;
 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.common.ObjectSerDeUtils;
 import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
-import org.apache.pinot.segment.local.customobject.LongLongPair;
+import org.apache.pinot.segment.local.customobject.BigDecimalLongPair;
 import org.apache.pinot.segment.local.customobject.ValueLongPair;
 
 
-/// This function is used for FirstWithTime calculations for data column with 
long type.
+/// This function is used for FirstWithTime calculations for data column with 
big decimal type.
 ///
-/// The function can be used as FirstWithTime(dataExpression, timeExpression, 
'long')
+/// The function can be used as FirstWithTime(dataExpression, timeExpression, 
'big_decimal')
 ///
 /// Following arguments are supported:
 ///
-/// - dataExpression: expression that contains the long data column to be 
calculated first on
+/// - dataExpression: expression that contains the big decimal data column to 
be calculated first on
 /// - timeExpression: expression that contains the column to be used to decide 
which data is first, can be any
 ///   Numeric column
-public class FirstLongValueWithTimeAggregationFunction extends 
FirstWithTimeAggregationFunction<Long> {
-  private final static ValueLongPair<Long> DEFAULT_VALUE_TIME_PAIR = new 
LongLongPair(Long.MIN_VALUE, Long.MAX_VALUE);
+public class FirstBigDecimalValueWithTimeAggregationFunction extends 
FirstWithTimeAggregationFunction<BigDecimal> {
+  private final static ValueLongPair<BigDecimal> DEFAULT_VALUE_TIME_PAIR =
+      new BigDecimalLongPair(BigDecimal.ZERO, Long.MAX_VALUE);
 
-  public FirstLongValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
+  public FirstBigDecimalValueWithTimeAggregationFunction(ExpressionContext 
dataCol, ExpressionContext timeCol,
       boolean nullHandlingEnabled) {
-    super(dataCol, timeCol, ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
+    super(dataCol, timeCol, ObjectSerDeUtils.BIG_DECIMAL_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
   }
 
   @Override
-  public ValueLongPair<Long> constructValueLongPair(Long value, long time) {
-    return new LongLongPair(value, time);
+  public ValueLongPair<BigDecimal> constructValueLongPair(BigDecimal value, 
long time) {
+    return new BigDecimalLongPair(value, time);
   }
 
   @Override
-  public ValueLongPair<Long> getDefaultValueTimePair() {
+  public ValueLongPair<BigDecimal> getDefaultValueTimePair() {
     return DEFAULT_VALUE_TIME_PAIR;
   }
 
   @Override
-  public Long readCell(BlockValSet block, int docId) {
-    return block.getLongValuesSV()[docId];
+  public BigDecimal readCell(BlockValSet block, int docId) {
+    return block.getBigDecimalValuesSV()[docId];
   }
 
   @Override
   public void aggregateGroupResultWithRawDataSv(int length, int[] 
groupKeyArray,
       GroupByResultHolder groupByResultHolder, BlockValSet blockValSet, 
BlockValSet timeValSet) {
-    long[] longValues = blockValSet.getLongValuesSV();
+    BigDecimal[] bigDecimalValues = blockValSet.getBigDecimalValuesSV();
     long[] timeValues = timeValSet.getLongValuesSV();
 
     forEachNotNull(length, blockValSet, timeValSet, (from, to) -> {
       for (int i = from; i < to; i++) {
-        long data = longValues[i];
+        BigDecimal data = bigDecimalValues[i];
         long time = timeValues[i];
         setGroupByResult(groupKeyArray[i], groupByResultHolder, data, time);
       }
@@ -78,12 +80,12 @@ public class FirstLongValueWithTimeAggregationFunction 
extends FirstWithTimeAggr
   @Override
   public void aggregateGroupResultWithRawDataMv(int length, int[][] 
groupKeysArray,
       GroupByResultHolder groupByResultHolder, BlockValSet blockValSet, 
BlockValSet timeValSet) {
-    long[] longValues = blockValSet.getLongValuesSV();
+    BigDecimal[] bigDecimalValues = blockValSet.getBigDecimalValuesSV();
     long[] timeValues = timeValSet.getLongValuesSV();
 
     forEachNotNull(length, blockValSet, timeValSet, (from, to) -> {
       for (int i = from; i < to; i++) {
-        long value = longValues[i];
+        BigDecimal value = bigDecimalValues[i];
         long time = timeValues[i];
         for (int groupKey : groupKeysArray[i]) {
           setGroupByResult(groupKey, groupByResultHolder, value, time);
@@ -94,22 +96,22 @@ public class FirstLongValueWithTimeAggregationFunction 
extends FirstWithTimeAggr
 
   @Override
   public String getResultColumnName() {
-    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'LONG')";
+    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'BIG_DECIMAL')";
   }
 
   @Override
-  public SerializedIntermediateResult 
serializeIntermediateResult(ValueLongPair<Long> longLongPair) {
-    return new 
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.LongLongPair.getValue(),
-        ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE.serialize((LongLongPair) 
longLongPair));
+  public SerializedIntermediateResult 
serializeIntermediateResult(ValueLongPair<BigDecimal> bigDecimalLongPair) {
+    return new 
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.BigDecimalLongPair.getValue(),
+        
ObjectSerDeUtils.BIG_DECIMAL_LONG_PAIR_SER_DE.serialize((BigDecimalLongPair) 
bigDecimalLongPair));
   }
 
   @Override
-  public ValueLongPair<Long> deserializeIntermediateResult(CustomObject 
customObject) {
-    return 
ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE.deserialize(customObject.getBuffer());
+  public ValueLongPair<BigDecimal> deserializeIntermediateResult(CustomObject 
customObject) {
+    return 
ObjectSerDeUtils.BIG_DECIMAL_LONG_PAIR_SER_DE.deserialize(customObject.getBuffer());
   }
 
   @Override
   public ColumnDataType getFinalResultColumnType() {
-    return ColumnDataType.LONG;
+    return ColumnDataType.BIG_DECIMAL;
   }
 }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstBytesValueWithTimeAggregationFunction.java
similarity index 55%
copy from 
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
copy to 
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstBytesValueWithTimeAggregationFunction.java
index 382abe1be19..d3aec379777 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstBytesValueWithTimeAggregationFunction.java
@@ -18,57 +18,72 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import com.google.common.base.Preconditions;
 import org.apache.pinot.common.CustomObject;
 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.common.ObjectSerDeUtils;
 import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
-import org.apache.pinot.segment.local.customobject.LongLongPair;
+import org.apache.pinot.segment.local.customobject.BytesLongPair;
 import org.apache.pinot.segment.local.customobject.ValueLongPair;
+import org.apache.pinot.spi.utils.ByteArray;
 
 
-/// This function is used for FirstWithTime calculations for data column with 
long type.
+/// This function is used for FirstWithTime calculations for data column with 
bytes or UUID type.
 ///
-/// The function can be used as FirstWithTime(dataExpression, timeExpression, 
'long')
+/// The function can be used as FirstWithTime(dataExpression, timeExpression, 
'bytes')
 ///
 /// Following arguments are supported:
 ///
-/// - dataExpression: expression that contains the long data column to be 
calculated first on
+/// - dataExpression: expression that contains the bytes or UUID data column 
to be calculated first on
 /// - timeExpression: expression that contains the column to be used to decide 
which data is first, can be any
 ///   Numeric column
-public class FirstLongValueWithTimeAggregationFunction extends 
FirstWithTimeAggregationFunction<Long> {
-  private final static ValueLongPair<Long> DEFAULT_VALUE_TIME_PAIR = new 
LongLongPair(Long.MIN_VALUE, Long.MAX_VALUE);
+public class FirstBytesValueWithTimeAggregationFunction extends 
FirstWithTimeAggregationFunction<ByteArray> {
+  private final ColumnDataType _resultType;
+  private final ValueLongPair<ByteArray> _defaultValueTimePair;
 
-  public FirstLongValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
-      boolean nullHandlingEnabled) {
-    super(dataCol, timeCol, ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
+  /// The result type is BYTES or UUID; both are stored as bytes.
+  public FirstBytesValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
+      boolean nullHandlingEnabled, ColumnDataType resultType) {
+    super(dataCol, timeCol, ObjectSerDeUtils.BYTES_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
+    Preconditions.checkArgument(resultType == ColumnDataType.BYTES || 
resultType == ColumnDataType.UUID,
+        "Unsupported result type: %s", resultType);
+    _resultType = resultType;
+    // An empty value is not a valid UUID, so the default value is the result 
type's null placeholder (the nil UUID).
+    _defaultValueTimePair = new BytesLongPair((ByteArray) 
resultType.getNullPlaceholder(), Long.MAX_VALUE);
   }
 
   @Override
-  public ValueLongPair<Long> constructValueLongPair(Long value, long time) {
-    return new LongLongPair(value, time);
+  public ValueLongPair<ByteArray> constructValueLongPair(ByteArray value, long 
time) {
+    return new BytesLongPair(value, time);
   }
 
   @Override
-  public ValueLongPair<Long> getDefaultValueTimePair() {
-    return DEFAULT_VALUE_TIME_PAIR;
+  public ValueLongPair<ByteArray> getDefaultValueTimePair() {
+    return _defaultValueTimePair;
   }
 
+  /// A BYTES data column holds values, not serialized pairs.
   @Override
-  public Long readCell(BlockValSet block, int docId) {
-    return block.getLongValuesSV()[docId];
+  protected boolean isSerializedPairInput(BlockValSet blockValSet) {
+    return false;
+  }
+
+  @Override
+  public ByteArray readCell(BlockValSet block, int docId) {
+    return new ByteArray(block.getBytesValuesSV()[docId]);
   }
 
   @Override
   public void aggregateGroupResultWithRawDataSv(int length, int[] 
groupKeyArray,
       GroupByResultHolder groupByResultHolder, BlockValSet blockValSet, 
BlockValSet timeValSet) {
-    long[] longValues = blockValSet.getLongValuesSV();
+    byte[][] bytesValues = blockValSet.getBytesValuesSV();
     long[] timeValues = timeValSet.getLongValuesSV();
 
     forEachNotNull(length, blockValSet, timeValSet, (from, to) -> {
       for (int i = from; i < to; i++) {
-        long data = longValues[i];
+        ByteArray data = new ByteArray(bytesValues[i]);
         long time = timeValues[i];
         setGroupByResult(groupKeyArray[i], groupByResultHolder, data, time);
       }
@@ -78,12 +93,12 @@ public class FirstLongValueWithTimeAggregationFunction 
extends FirstWithTimeAggr
   @Override
   public void aggregateGroupResultWithRawDataMv(int length, int[][] 
groupKeysArray,
       GroupByResultHolder groupByResultHolder, BlockValSet blockValSet, 
BlockValSet timeValSet) {
-    long[] longValues = blockValSet.getLongValuesSV();
+    byte[][] bytesValues = blockValSet.getBytesValuesSV();
     long[] timeValues = timeValSet.getLongValuesSV();
 
     forEachNotNull(length, blockValSet, timeValSet, (from, to) -> {
       for (int i = from; i < to; i++) {
-        long value = longValues[i];
+        ByteArray value = new ByteArray(bytesValues[i]);
         long time = timeValues[i];
         for (int groupKey : groupKeysArray[i]) {
           setGroupByResult(groupKey, groupByResultHolder, value, time);
@@ -94,22 +109,22 @@ public class FirstLongValueWithTimeAggregationFunction 
extends FirstWithTimeAggr
 
   @Override
   public String getResultColumnName() {
-    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'LONG')";
+    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'" + _resultType + "')";
   }
 
   @Override
-  public SerializedIntermediateResult 
serializeIntermediateResult(ValueLongPair<Long> longLongPair) {
-    return new 
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.LongLongPair.getValue(),
-        ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE.serialize((LongLongPair) 
longLongPair));
+  public SerializedIntermediateResult 
serializeIntermediateResult(ValueLongPair<ByteArray> bytesLongPair) {
+    return new 
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.BytesLongPair.getValue(),
+        ObjectSerDeUtils.BYTES_LONG_PAIR_SER_DE.serialize((BytesLongPair) 
bytesLongPair));
   }
 
   @Override
-  public ValueLongPair<Long> deserializeIntermediateResult(CustomObject 
customObject) {
-    return 
ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE.deserialize(customObject.getBuffer());
+  public ValueLongPair<ByteArray> deserializeIntermediateResult(CustomObject 
customObject) {
+    return 
ObjectSerDeUtils.BYTES_LONG_PAIR_SER_DE.deserialize(customObject.getBuffer());
   }
 
   @Override
   public ColumnDataType getFinalResultColumnType() {
-    return ColumnDataType.LONG;
+    return _resultType;
   }
 }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLastWithTimeAggregationFunctionProvider.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLastWithTimeAggregationFunctionProvider.java
index 35e7ca647e5..c6ecb8c91d6 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLastWithTimeAggregationFunctionProvider.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLastWithTimeAggregationFunctionProvider.java
@@ -22,6 +22,7 @@ import com.google.common.base.Preconditions;
 import java.util.List;
 import org.apache.pinot.common.request.context.ExpressionContext;
 import org.apache.pinot.common.request.context.FunctionContext;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
 
@@ -60,12 +61,24 @@ public final class 
FirstLastWithTimeAggregationFunctionProvider {
           return new FirstIntValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled, false);
         case LONG:
           return new FirstLongValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
+        case TIMESTAMP:
+          return new FirstLongValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled,
+              ColumnDataType.TIMESTAMP);
         case FLOAT:
           return new FirstFloatValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
         case DOUBLE:
           return new FirstDoubleValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
+        case BIG_DECIMAL:
+          return new FirstBigDecimalValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
         case STRING:
+        case JSON:
           return new FirstStringValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
+        case BYTES:
+          return new FirstBytesValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled,
+              ColumnDataType.BYTES);
+        case UUID:
+          return new FirstBytesValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled,
+              ColumnDataType.UUID);
         default:
           throw new IllegalArgumentException("Unsupported data type for 
FIRST_WITH_TIME: " + dataType);
       }
@@ -100,12 +113,24 @@ public final class 
FirstLastWithTimeAggregationFunctionProvider {
           return new LastIntValueWithTimeAggregationFunction(dataCol, timeCol, 
nullHandlingEnabled, false);
         case LONG:
           return new LastLongValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
+        case TIMESTAMP:
+          return new LastLongValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled,
+              ColumnDataType.TIMESTAMP);
         case FLOAT:
           return new LastFloatValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
         case DOUBLE:
           return new LastDoubleValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
+        case BIG_DECIMAL:
+          return new LastBigDecimalValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
         case STRING:
+        case JSON:
           return new LastStringValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled);
+        case BYTES:
+          return new LastBytesValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled,
+              ColumnDataType.BYTES);
+        case UUID:
+          return new LastBytesValueWithTimeAggregationFunction(dataCol, 
timeCol, nullHandlingEnabled,
+              ColumnDataType.UUID);
         default:
           throw new IllegalArgumentException("Unsupported data type for 
LAST_WITH_TIME: " + dataType);
       }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
index 382abe1be19..e56f309a0d6 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstLongValueWithTimeAggregationFunction.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import com.google.common.base.Preconditions;
 import org.apache.pinot.common.CustomObject;
 import org.apache.pinot.common.request.context.ExpressionContext;
 import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
@@ -40,9 +41,20 @@ import 
org.apache.pinot.segment.local.customobject.ValueLongPair;
 public class FirstLongValueWithTimeAggregationFunction extends 
FirstWithTimeAggregationFunction<Long> {
   private final static ValueLongPair<Long> DEFAULT_VALUE_TIME_PAIR = new 
LongLongPair(Long.MIN_VALUE, Long.MAX_VALUE);
 
+  private final ColumnDataType _resultType;
+
   public FirstLongValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
       boolean nullHandlingEnabled) {
+    this(dataCol, timeCol, nullHandlingEnabled, ColumnDataType.LONG);
+  }
+
+  /// The result type is LONG or TIMESTAMP; both are stored as long values.
+  public FirstLongValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
+      boolean nullHandlingEnabled, ColumnDataType resultType) {
     super(dataCol, timeCol, ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
+    Preconditions.checkArgument(resultType == ColumnDataType.LONG || 
resultType == ColumnDataType.TIMESTAMP,
+        "Unsupported result type: %s", resultType);
+    _resultType = resultType;
   }
 
   @Override
@@ -94,7 +106,7 @@ public class FirstLongValueWithTimeAggregationFunction 
extends FirstWithTimeAggr
 
   @Override
   public String getResultColumnName() {
-    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'LONG')";
+    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'" + _resultType + "')";
   }
 
   @Override
@@ -110,6 +122,6 @@ public class FirstLongValueWithTimeAggregationFunction 
extends FirstWithTimeAggr
 
   @Override
   public ColumnDataType getFinalResultColumnType() {
-    return ColumnDataType.LONG;
+    return _resultType;
   }
 }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstWithTimeAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstWithTimeAggregationFunction.java
index f41e4f63423..f10a28cf746 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstWithTimeAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/FirstWithTimeAggregationFunction.java
@@ -64,6 +64,12 @@ public abstract class FirstWithTimeAggregationFunction<V 
extends Comparable<V>>
 
   public abstract ValueLongPair<V> getDefaultValueTimePair();
 
+  /// Whether the data input holds serialized value-time pairs, as a 
pre-aggregated BYTES column does, rather than data
+  /// values.
+  protected boolean isSerializedPairInput(BlockValSet blockValSet) {
+    return blockValSet.getValueType() == DataType.BYTES;
+  }
+
   public abstract V readCell(BlockValSet block, int docId);
 
   public abstract void aggregateGroupResultWithRawDataSv(int length,
@@ -99,7 +105,7 @@ public abstract class FirstWithTimeAggregationFunction<V 
extends Comparable<V>>
 
     BlockValSet blockValSet = blockValSetMap.get(_expression);
     BlockValSet blockTimeSet = blockValSetMap.get(_timeCol);
-    if (blockValSet.getValueType() != DataType.BYTES) {
+    if (!isSerializedPairInput(blockValSet)) {
       IntLongPair defaultPair = new IntLongPair(Integer.MIN_VALUE, 
Long.MAX_VALUE);
       long[] timeValues = blockTimeSet.getLongValuesSV();
 
@@ -154,7 +160,7 @@ public abstract class FirstWithTimeAggregationFunction<V 
extends Comparable<V>>
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
     BlockValSet timeValSet = blockValSetMap.get(_timeCol);
-    if (blockValSet.getValueType() != DataType.BYTES) {
+    if (!isSerializedPairInput(blockValSet)) {
       aggregateGroupResultWithRawDataSv(length, groupKeyArray, 
groupByResultHolder,
           blockValSet, timeValSet);
     } else {
@@ -177,7 +183,7 @@ public abstract class FirstWithTimeAggregationFunction<V 
extends Comparable<V>>
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
     BlockValSet timeValSet = blockValSetMap.get(_timeCol);
-    if (blockValSet.getValueType() != DataType.BYTES) {
+    if (!isSerializedPairInput(blockValSet)) {
       aggregateGroupResultWithRawDataMv(length, groupKeysArray, 
groupByResultHolder, blockValSet, timeValSet);
     } else {
       // Serialized ValueTimePair
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastBigDecimalValueWithTimeAggregationFunction.java
similarity index 64%
copy from 
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
copy to 
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastBigDecimalValueWithTimeAggregationFunction.java
index 0e543204709..3c4be3a02fe 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastBigDecimalValueWithTimeAggregationFunction.java
@@ -18,57 +18,59 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import java.math.BigDecimal;
 import org.apache.pinot.common.CustomObject;
 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.common.ObjectSerDeUtils;
 import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
-import org.apache.pinot.segment.local.customobject.LongLongPair;
+import org.apache.pinot.segment.local.customobject.BigDecimalLongPair;
 import org.apache.pinot.segment.local.customobject.ValueLongPair;
 
 
-/// This function is used for LastWithTime calculations for data column with 
long type.
+/// This function is used for LastWithTime calculations for data column with 
big decimal type.
 ///
-/// The function can be used as LastWithTime(dataExpression, timeExpression, 
'long')
+/// The function can be used as LastWithTime(dataExpression, timeExpression, 
'big_decimal')
 ///
 /// Following arguments are supported:
 ///
-/// - dataExpression: expression that contains the long data column to be 
calculated last on
+/// - dataExpression: expression that contains the big decimal data column to 
be calculated last on
 /// - timeExpression: expression that contains the column to be used to decide 
which data is last, can be any
 ///   Numeric column
-public class LastLongValueWithTimeAggregationFunction extends 
LastWithTimeAggregationFunction<Long> {
-  private final static ValueLongPair<Long> DEFAULT_VALUE_TIME_PAIR = new 
LongLongPair(Long.MIN_VALUE, Long.MIN_VALUE);
+public class LastBigDecimalValueWithTimeAggregationFunction extends 
LastWithTimeAggregationFunction<BigDecimal> {
+  private final static ValueLongPair<BigDecimal> DEFAULT_VALUE_TIME_PAIR =
+      new BigDecimalLongPair(BigDecimal.ZERO, Long.MIN_VALUE);
 
-  public LastLongValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
+  public LastBigDecimalValueWithTimeAggregationFunction(ExpressionContext 
dataCol, ExpressionContext timeCol,
       boolean nullHandlingEnabled) {
-    super(dataCol, timeCol, ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
+    super(dataCol, timeCol, ObjectSerDeUtils.BIG_DECIMAL_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
   }
 
   @Override
-  public ValueLongPair<Long> constructValueLongPair(Long value, long time) {
-    return new LongLongPair(value, time);
+  public ValueLongPair<BigDecimal> constructValueLongPair(BigDecimal value, 
long time) {
+    return new BigDecimalLongPair(value, time);
   }
 
   @Override
-  public ValueLongPair<Long> getDefaultValueTimePair() {
+  public ValueLongPair<BigDecimal> getDefaultValueTimePair() {
     return DEFAULT_VALUE_TIME_PAIR;
   }
 
   @Override
-  public Long readCell(BlockValSet block, int docId) {
-    return block.getLongValuesSV()[docId];
+  public BigDecimal readCell(BlockValSet block, int docId) {
+    return block.getBigDecimalValuesSV()[docId];
   }
 
   @Override
   public void aggregateGroupResultWithRawDataSv(int length, int[] 
groupKeyArray,
       GroupByResultHolder groupByResultHolder, BlockValSet blockValSet, 
BlockValSet timeValSet) {
-    long[] longValues = blockValSet.getLongValuesSV();
+    BigDecimal[] bigDecimalValues = blockValSet.getBigDecimalValuesSV();
     long[] timeValues = timeValSet.getLongValuesSV();
 
     forEachNotNull(length, blockValSet, timeValSet, (from, to) -> {
       for (int i = from; i < to; i++) {
-        long data = longValues[i];
+        BigDecimal data = bigDecimalValues[i];
         long time = timeValues[i];
         setGroupByResult(groupKeyArray[i], groupByResultHolder, data, time);
       }
@@ -78,12 +80,12 @@ public class LastLongValueWithTimeAggregationFunction 
extends LastWithTimeAggreg
   @Override
   public void aggregateGroupResultWithRawDataMv(int length, int[][] 
groupKeysArray,
       GroupByResultHolder groupByResultHolder, BlockValSet blockValSet, 
BlockValSet timeValSet) {
-    long[] longValues = blockValSet.getLongValuesSV();
+    BigDecimal[] bigDecimalValues = blockValSet.getBigDecimalValuesSV();
     long[] timeValues = timeValSet.getLongValuesSV();
 
     forEachNotNull(length, blockValSet, timeValSet, (from, to) -> {
       for (int i = from; i < to; i++) {
-        long value = longValues[i];
+        BigDecimal value = bigDecimalValues[i];
         long time = timeValues[i];
         for (int groupKey : groupKeysArray[i]) {
           setGroupByResult(groupKey, groupByResultHolder, value, time);
@@ -94,22 +96,22 @@ public class LastLongValueWithTimeAggregationFunction 
extends LastWithTimeAggreg
 
   @Override
   public String getResultColumnName() {
-    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'LONG')";
+    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'BIG_DECIMAL')";
   }
 
   @Override
-  public SerializedIntermediateResult 
serializeIntermediateResult(ValueLongPair<Long> longLongPair) {
-    return new 
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.LongLongPair.getValue(),
-        ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE.serialize((LongLongPair) 
longLongPair));
+  public SerializedIntermediateResult 
serializeIntermediateResult(ValueLongPair<BigDecimal> bigDecimalLongPair) {
+    return new 
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.BigDecimalLongPair.getValue(),
+        
ObjectSerDeUtils.BIG_DECIMAL_LONG_PAIR_SER_DE.serialize((BigDecimalLongPair) 
bigDecimalLongPair));
   }
 
   @Override
-  public ValueLongPair<Long> deserializeIntermediateResult(CustomObject 
customObject) {
-    return 
ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE.deserialize(customObject.getBuffer());
+  public ValueLongPair<BigDecimal> deserializeIntermediateResult(CustomObject 
customObject) {
+    return 
ObjectSerDeUtils.BIG_DECIMAL_LONG_PAIR_SER_DE.deserialize(customObject.getBuffer());
   }
 
   @Override
   public ColumnDataType getFinalResultColumnType() {
-    return ColumnDataType.LONG;
+    return ColumnDataType.BIG_DECIMAL;
   }
 }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastBytesValueWithTimeAggregationFunction.java
similarity index 55%
copy from 
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
copy to 
pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastBytesValueWithTimeAggregationFunction.java
index 0e543204709..338092948e2 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastBytesValueWithTimeAggregationFunction.java
@@ -18,57 +18,72 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import com.google.common.base.Preconditions;
 import org.apache.pinot.common.CustomObject;
 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.common.ObjectSerDeUtils;
 import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
-import org.apache.pinot.segment.local.customobject.LongLongPair;
+import org.apache.pinot.segment.local.customobject.BytesLongPair;
 import org.apache.pinot.segment.local.customobject.ValueLongPair;
+import org.apache.pinot.spi.utils.ByteArray;
 
 
-/// This function is used for LastWithTime calculations for data column with 
long type.
+/// This function is used for LastWithTime calculations for data column with 
bytes or UUID type.
 ///
-/// The function can be used as LastWithTime(dataExpression, timeExpression, 
'long')
+/// The function can be used as LastWithTime(dataExpression, timeExpression, 
'bytes')
 ///
 /// Following arguments are supported:
 ///
-/// - dataExpression: expression that contains the long data column to be 
calculated last on
+/// - dataExpression: expression that contains the bytes or UUID data column 
to be calculated last on
 /// - timeExpression: expression that contains the column to be used to decide 
which data is last, can be any
 ///   Numeric column
-public class LastLongValueWithTimeAggregationFunction extends 
LastWithTimeAggregationFunction<Long> {
-  private final static ValueLongPair<Long> DEFAULT_VALUE_TIME_PAIR = new 
LongLongPair(Long.MIN_VALUE, Long.MIN_VALUE);
+public class LastBytesValueWithTimeAggregationFunction extends 
LastWithTimeAggregationFunction<ByteArray> {
+  private final ColumnDataType _resultType;
+  private final ValueLongPair<ByteArray> _defaultValueTimePair;
 
-  public LastLongValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
-      boolean nullHandlingEnabled) {
-    super(dataCol, timeCol, ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
+  /// The result type is BYTES or UUID; both are stored as bytes.
+  public LastBytesValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
+      boolean nullHandlingEnabled, ColumnDataType resultType) {
+    super(dataCol, timeCol, ObjectSerDeUtils.BYTES_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
+    Preconditions.checkArgument(resultType == ColumnDataType.BYTES || 
resultType == ColumnDataType.UUID,
+        "Unsupported result type: %s", resultType);
+    _resultType = resultType;
+    // An empty value is not a valid UUID, so the default value is the result 
type's null placeholder (the nil UUID).
+    _defaultValueTimePair = new BytesLongPair((ByteArray) 
resultType.getNullPlaceholder(), Long.MIN_VALUE);
   }
 
   @Override
-  public ValueLongPair<Long> constructValueLongPair(Long value, long time) {
-    return new LongLongPair(value, time);
+  public ValueLongPair<ByteArray> constructValueLongPair(ByteArray value, long 
time) {
+    return new BytesLongPair(value, time);
   }
 
   @Override
-  public ValueLongPair<Long> getDefaultValueTimePair() {
-    return DEFAULT_VALUE_TIME_PAIR;
+  public ValueLongPair<ByteArray> getDefaultValueTimePair() {
+    return _defaultValueTimePair;
   }
 
+  /// A BYTES data column holds values, not serialized pairs.
   @Override
-  public Long readCell(BlockValSet block, int docId) {
-    return block.getLongValuesSV()[docId];
+  protected boolean isSerializedPairInput(BlockValSet blockValSet) {
+    return false;
+  }
+
+  @Override
+  public ByteArray readCell(BlockValSet block, int docId) {
+    return new ByteArray(block.getBytesValuesSV()[docId]);
   }
 
   @Override
   public void aggregateGroupResultWithRawDataSv(int length, int[] 
groupKeyArray,
       GroupByResultHolder groupByResultHolder, BlockValSet blockValSet, 
BlockValSet timeValSet) {
-    long[] longValues = blockValSet.getLongValuesSV();
+    byte[][] bytesValues = blockValSet.getBytesValuesSV();
     long[] timeValues = timeValSet.getLongValuesSV();
 
     forEachNotNull(length, blockValSet, timeValSet, (from, to) -> {
       for (int i = from; i < to; i++) {
-        long data = longValues[i];
+        ByteArray data = new ByteArray(bytesValues[i]);
         long time = timeValues[i];
         setGroupByResult(groupKeyArray[i], groupByResultHolder, data, time);
       }
@@ -78,12 +93,12 @@ public class LastLongValueWithTimeAggregationFunction 
extends LastWithTimeAggreg
   @Override
   public void aggregateGroupResultWithRawDataMv(int length, int[][] 
groupKeysArray,
       GroupByResultHolder groupByResultHolder, BlockValSet blockValSet, 
BlockValSet timeValSet) {
-    long[] longValues = blockValSet.getLongValuesSV();
+    byte[][] bytesValues = blockValSet.getBytesValuesSV();
     long[] timeValues = timeValSet.getLongValuesSV();
 
     forEachNotNull(length, blockValSet, timeValSet, (from, to) -> {
       for (int i = from; i < to; i++) {
-        long value = longValues[i];
+        ByteArray value = new ByteArray(bytesValues[i]);
         long time = timeValues[i];
         for (int groupKey : groupKeysArray[i]) {
           setGroupByResult(groupKey, groupByResultHolder, value, time);
@@ -94,22 +109,22 @@ public class LastLongValueWithTimeAggregationFunction 
extends LastWithTimeAggreg
 
   @Override
   public String getResultColumnName() {
-    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'LONG')";
+    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'" + _resultType + "')";
   }
 
   @Override
-  public SerializedIntermediateResult 
serializeIntermediateResult(ValueLongPair<Long> longLongPair) {
-    return new 
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.LongLongPair.getValue(),
-        ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE.serialize((LongLongPair) 
longLongPair));
+  public SerializedIntermediateResult 
serializeIntermediateResult(ValueLongPair<ByteArray> bytesLongPair) {
+    return new 
SerializedIntermediateResult(ObjectSerDeUtils.ObjectType.BytesLongPair.getValue(),
+        ObjectSerDeUtils.BYTES_LONG_PAIR_SER_DE.serialize((BytesLongPair) 
bytesLongPair));
   }
 
   @Override
-  public ValueLongPair<Long> deserializeIntermediateResult(CustomObject 
customObject) {
-    return 
ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE.deserialize(customObject.getBuffer());
+  public ValueLongPair<ByteArray> deserializeIntermediateResult(CustomObject 
customObject) {
+    return 
ObjectSerDeUtils.BYTES_LONG_PAIR_SER_DE.deserialize(customObject.getBuffer());
   }
 
   @Override
   public ColumnDataType getFinalResultColumnType() {
-    return ColumnDataType.LONG;
+    return _resultType;
   }
 }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
index 0e543204709..3d5250282d4 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastLongValueWithTimeAggregationFunction.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import com.google.common.base.Preconditions;
 import org.apache.pinot.common.CustomObject;
 import org.apache.pinot.common.request.context.ExpressionContext;
 import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
@@ -40,9 +41,20 @@ import 
org.apache.pinot.segment.local.customobject.ValueLongPair;
 public class LastLongValueWithTimeAggregationFunction extends 
LastWithTimeAggregationFunction<Long> {
   private final static ValueLongPair<Long> DEFAULT_VALUE_TIME_PAIR = new 
LongLongPair(Long.MIN_VALUE, Long.MIN_VALUE);
 
+  private final ColumnDataType _resultType;
+
   public LastLongValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
       boolean nullHandlingEnabled) {
+    this(dataCol, timeCol, nullHandlingEnabled, ColumnDataType.LONG);
+  }
+
+  /// The result type is LONG or TIMESTAMP; both are stored as long values.
+  public LastLongValueWithTimeAggregationFunction(ExpressionContext dataCol, 
ExpressionContext timeCol,
+      boolean nullHandlingEnabled, ColumnDataType resultType) {
     super(dataCol, timeCol, ObjectSerDeUtils.LONG_LONG_PAIR_SER_DE, 
nullHandlingEnabled);
+    Preconditions.checkArgument(resultType == ColumnDataType.LONG || 
resultType == ColumnDataType.TIMESTAMP,
+        "Unsupported result type: %s", resultType);
+    _resultType = resultType;
   }
 
   @Override
@@ -94,7 +106,7 @@ public class LastLongValueWithTimeAggregationFunction 
extends LastWithTimeAggreg
 
   @Override
   public String getResultColumnName() {
-    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'LONG')";
+    return getType().getName().toLowerCase() + "(" + _expression + "," + 
_timeCol + ",'" + _resultType + "')";
   }
 
   @Override
@@ -110,6 +122,6 @@ public class LastLongValueWithTimeAggregationFunction 
extends LastWithTimeAggreg
 
   @Override
   public ColumnDataType getFinalResultColumnType() {
-    return ColumnDataType.LONG;
+    return _resultType;
   }
 }
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastWithTimeAggregationFunction.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastWithTimeAggregationFunction.java
index 4896a43fd71..a1bd5d2e529 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastWithTimeAggregationFunction.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/aggregation/function/LastWithTimeAggregationFunction.java
@@ -65,6 +65,12 @@ public abstract class LastWithTimeAggregationFunction<V 
extends Comparable<V>>
 
   public abstract ValueLongPair<V> getDefaultValueTimePair();
 
+  /// Whether the data input holds serialized value-time pairs, as a 
pre-aggregated BYTES column does, rather than data
+  /// values.
+  protected boolean isSerializedPairInput(BlockValSet blockValSet) {
+    return blockValSet.getValueType() == DataType.BYTES;
+  }
+
   public abstract V readCell(BlockValSet block, int docId);
 
   public abstract void aggregateGroupResultWithRawDataSv(int length,
@@ -100,7 +106,7 @@ public abstract class LastWithTimeAggregationFunction<V 
extends Comparable<V>>
 
     BlockValSet blockValSet = blockValSetMap.get(_expression);
     BlockValSet blockTimeSet = blockValSetMap.get(_timeCol);
-    if (blockValSet.getValueType() != DataType.BYTES) {
+    if (!isSerializedPairInput(blockValSet)) {
       IntLongPair defaultPair = new IntLongPair(Integer.MIN_VALUE, 
Long.MIN_VALUE);
       long[] timeValues = blockTimeSet.getLongValuesSV();
 
@@ -154,7 +160,7 @@ public abstract class LastWithTimeAggregationFunction<V 
extends Comparable<V>>
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
     BlockValSet timeValSet = blockValSetMap.get(_timeCol);
-    if (blockValSet.getValueType() != DataType.BYTES) {
+    if (!isSerializedPairInput(blockValSet)) {
       aggregateGroupResultWithRawDataSv(length, groupKeyArray, 
groupByResultHolder,
           blockValSet, timeValSet);
     } else {
@@ -175,7 +181,7 @@ public abstract class LastWithTimeAggregationFunction<V 
extends Comparable<V>>
       Map<ExpressionContext, BlockValSet> blockValSetMap) {
     BlockValSet blockValSet = blockValSetMap.get(_expression);
     BlockValSet timeValSet = blockValSetMap.get(_timeCol);
-    if (blockValSet.getValueType() != DataType.BYTES) {
+    if (!isSerializedPairInput(blockValSet)) {
       aggregateGroupResultWithRawDataMv(length, groupKeysArray, 
groupByResultHolder, blockValSet, timeValSet);
     } else {
       // Serialized ValueTimePair
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizer.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizer.java
index f88c1cf372d..e7ed844ff79 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizer.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizer.java
@@ -23,21 +23,29 @@ import javax.annotation.Nullable;
 import org.apache.pinot.common.request.Expression;
 import org.apache.pinot.common.request.Function;
 import org.apache.pinot.common.request.PinotQuery;
+import org.apache.pinot.common.utils.request.RequestUtils;
 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.apache.pinot.spi.utils.CommonConstants;
 
 
 /// Rewrites certain aggregation functions based on operand types to support 
polymorphic aggregations.
 ///
-/// Currently supported rewrites:
+/// Always applied:
+/// - FIRST_WITH_TIME(dataColumn, timeColumn) -> FIRST_WITH_TIME(dataColumn, 
timeColumn, 'dataType')
+/// - LAST_WITH_TIME(dataColumn, timeColumn) -> LAST_WITH_TIME(dataColumn, 
timeColumn, 'dataType')
+///
+/// Applied only when enabled through the `autoRewriteAggregationType` query 
option:
 /// - MIN(stringType) -> MINSTRING
 /// - MAX(stringType) -> MAXSTRING
 /// - MIN(longType) -> MINLONG
 /// - MAX(longType) -> MAXLONG
 /// - SUM(longType) -> SUMLONG
 /// - SUM(intType) -> SUMINT
+///
+/// Aggregations nested in other expressions, such as an alias, a FILTER 
clause or a transform, are rewritten as well.
 public class AggregateFunctionRewriteOptimizer implements StatementOptimizer {
 
   @Override
@@ -46,38 +54,24 @@ public class AggregateFunctionRewriteOptimizer implements 
StatementOptimizer {
       return;
     }
 
-    // Only perform auto rewrite when enabled through query option.
-    if (pinotQuery.getQueryOptions() == null || 
!Boolean.parseBoolean(pinotQuery.getQueryOptions().get(
-        
CommonConstants.Broker.Request.QueryOptionKey.AUTO_REWRITE_AGGREGATION_TYPE))) {
-      return;
-    }
-
-    List<Expression> selectList = pinotQuery.getSelectList();
-    if (selectList != null) {
-      for (Expression expression : selectList) {
-        maybeRewriteAggregateFunction(expression, schema);
-      }
-    }
-
-    List<Expression> groupByList = pinotQuery.getGroupByList();
-    if (groupByList != null) {
-      for (Expression expression : groupByList) {
-        maybeRewriteAggregateFunction(expression, schema);
-      }
-    }
+    boolean rewriteToTypedVariants = pinotQuery.getQueryOptions() != null && 
Boolean.parseBoolean(
+        
pinotQuery.getQueryOptions().get(CommonConstants.Broker.Request.QueryOptionKey.AUTO_REWRITE_AGGREGATION_TYPE));
+    rewrite(pinotQuery.getSelectList(), schema, rewriteToTypedVariants);
+    rewrite(pinotQuery.getGroupByList(), schema, rewriteToTypedVariants);
+    rewrite(pinotQuery.getOrderByList(), schema, rewriteToTypedVariants);
+    rewrite(pinotQuery.getFilterExpression(), schema, rewriteToTypedVariants);
+    rewrite(pinotQuery.getHavingExpression(), schema, rewriteToTypedVariants);
+  }
 
-    List<Expression> orderByList = pinotQuery.getOrderByList();
-    if (orderByList != null) {
-      for (Expression expression : orderByList) {
-        maybeRewriteAggregateFunction(expression, schema);
+  private static void rewrite(@Nullable List<Expression> expressions, Schema 
schema, boolean rewriteToTypedVariants) {
+    if (expressions != null) {
+      for (Expression expression : expressions) {
+        rewrite(expression, schema, rewriteToTypedVariants);
       }
     }
-
-    maybeRewriteAggregateFunction(pinotQuery.getFilterExpression(), schema);
-    maybeRewriteAggregateFunction(pinotQuery.getHavingExpression(), schema);
   }
 
-  private void maybeRewriteAggregateFunction(@Nullable Expression expression, 
Schema schema) {
+  private static void rewrite(@Nullable Expression expression, Schema schema, 
boolean rewriteToTypedVariants) {
     if (expression == null || !expression.isSetFunctionCall()) {
       return;
     }
@@ -85,23 +79,62 @@ public class AggregateFunctionRewriteOptimizer implements 
StatementOptimizer {
     Function function = expression.getFunctionCall();
     String functionName = function.getOperator();
     if (!AggregationFunctionType.isAggregationFunction(functionName)) {
+      rewrite(function.getOperands(), schema, rewriteToTypedVariants);
       return;
     }
 
-    Expression operand = function.getOperands().get(0);
-    FieldSpec.DataType operandType;
     // TODO: Handle more complex expressions (e.g. MIN(trim(stringCol)) )
-    if (operand.isSetIdentifier()) {
-      String columnName = operand.getIdentifier().getName();
-      FieldSpec fieldSpec = schema.getFieldSpecFor(columnName);
-      if (fieldSpec == null) {
-        return;
-      }
-      operandType = fieldSpec.getDataType().getStoredType();
-    } else {
+    if (function.getOperandsSize() == 0 || 
!function.getOperands().get(0).isSetIdentifier()) {
+      return;
+    }
+    FieldSpec fieldSpec = 
schema.getFieldSpecFor(function.getOperands().get(0).getIdentifier().getName());
+    if (fieldSpec == null) {
+      return;
+    }
+
+    String normalizedName = 
AggregationFunctionType.getNormalizedAggregationFunctionName(functionName);
+    if ((normalizedName.equals(AggregationFunctionType.FIRSTWITHTIME.name())
+        || normalizedName.equals(AggregationFunctionType.LASTWITHTIME.name())) 
&& function.getOperandsSize() == 2) {
+      addDataTypeArgument(function, normalizedName, fieldSpec);
       return;
     }
+    if (rewriteToTypedVariants) {
+      // The typed variants follow the stored type (e.g. TIMESTAMP as LONG), 
while the data type argument above keeps
+      // the logical type.
+      rewriteToTypedVariant(function, functionName, 
fieldSpec.getDataType().getStoredType());
+    }
+  }
+
+  /// Appends the data column's logical type. The supported types match 
`PinotAggregateTypeArgumentRewriteRule` in the
+  /// multi-stage engine.
+  private static void addDataTypeArgument(Function function, String 
functionName, FieldSpec fieldSpec) {
+    if (fieldSpec.isSingleValueField()) {
+      switch (fieldSpec.getDataType()) {
+        case INT:
+        case LONG:
+        case FLOAT:
+        case DOUBLE:
+        case BIG_DECIMAL:
+        case STRING:
+        case BOOLEAN:
+        case TIMESTAMP:
+        case BYTES:
+        case UUID:
+          
function.addToOperands(RequestUtils.getLiteralExpression(fieldSpec.getDataType().name()));
+          return;
+        case JSON:
+          // Same as the multi-stage engine, which types a JSON column as 
VARCHAR.
+          
function.addToOperands(RequestUtils.getLiteralExpression(FieldSpec.DataType.STRING.name()));
+          return;
+        default:
+          break;
+      }
+    }
+    throw new BadQueryRequestException(String.format("Unsupported data type 
for %s(dataColumn, timeColumn): %s%s",
+        functionName, fieldSpec.isSingleValueField() ? "" : "multi-value ", 
fieldSpec.getDataType()));
+  }
 
+  private static void rewriteToTypedVariant(Function function, String 
functionName, FieldSpec.DataType operandType) {
     // Rewrite MIN(stringCol) and MAX(stringCol) to MINSTRING / MAXSTRING
     // Rewrite MIN(longCol) and MAX(longCol) to MINLONG / MAXLONG
     if ((functionName.equals(AggregationFunctionType.MIN.getName())
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/common/ObjectSerDeUtilsTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/common/ObjectSerDeUtilsTest.java
index 41c969962a5..d02250c778c 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/common/ObjectSerDeUtilsTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/common/ObjectSerDeUtilsTest.java
@@ -32,6 +32,7 @@ import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
 import it.unimi.dsi.fastutil.longs.LongArrayList;
 import it.unimi.dsi.fastutil.objects.ObjectArrayList;
 import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
+import java.math.BigDecimal;
 import java.nio.BufferOverflowException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
@@ -60,6 +61,8 @@ import 
org.apache.pinot.core.query.aggregation.function.PercentileEstAggregation
 import 
org.apache.pinot.core.query.aggregation.function.PercentileTDigestAggregationFunction;
 import org.apache.pinot.core.query.aggregation.function.funnel.FunnelStepEvent;
 import org.apache.pinot.segment.local.customobject.AvgPair;
+import org.apache.pinot.segment.local.customobject.BigDecimalLongPair;
+import org.apache.pinot.segment.local.customobject.BytesLongPair;
 import org.apache.pinot.segment.local.customobject.CpcSketchAccumulator;
 import org.apache.pinot.segment.local.customobject.DoubleLongPair;
 import org.apache.pinot.segment.local.customobject.FloatLongPair;
@@ -76,6 +79,7 @@ import 
org.apache.pinot.segment.local.customobject.tdigest.TDigest;
 import org.apache.pinot.segment.local.customobject.tdigest.TDigest.Centroid;
 import org.apache.pinot.segment.local.customobject.tdigest.TDigestAccumulator;
 import org.apache.pinot.segment.local.utils.UltraLogLogUtils;
+import org.apache.pinot.spi.utils.ByteArray;
 import org.roaringbitmap.RoaringBitmap;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
@@ -407,6 +411,36 @@ public class ObjectSerDeUtilsTest {
     }
   }
 
+  @Test
+  public void testBigDecimalValueTimePair() {
+    for (int i = 0; i < NUM_ITERATIONS; i++) {
+      ValueLongPair<BigDecimal> expected =
+          new BigDecimalLongPair(BigDecimal.valueOf(RANDOM.nextLong(), 
RANDOM.nextInt(20)), RANDOM.nextLong());
+
+      byte[] bytes = ObjectSerDeUtils.serialize(expected);
+      ValueLongPair<BigDecimal> actual =
+          ObjectSerDeUtils.deserialize(bytes, 
ObjectSerDeUtils.ObjectType.BigDecimalLongPair);
+
+      assertEquals(actual.getValue(), expected.getValue(), ERROR_MESSAGE);
+      assertEquals(actual.getTime(), expected.getTime(), ERROR_MESSAGE);
+    }
+  }
+
+  @Test
+  public void testBytesValueTimePair() {
+    for (int i = 0; i < NUM_ITERATIONS; i++) {
+      byte[] value = new byte[RANDOM.nextInt(32)];
+      RANDOM.nextBytes(value);
+      ValueLongPair<ByteArray> expected = new BytesLongPair(new 
ByteArray(value), RANDOM.nextLong());
+
+      byte[] bytes = ObjectSerDeUtils.serialize(expected);
+      ValueLongPair<ByteArray> actual = ObjectSerDeUtils.deserialize(bytes, 
ObjectSerDeUtils.ObjectType.BytesLongPair);
+
+      assertEquals(actual.getValue(), expected.getValue(), ERROR_MESSAGE);
+      assertEquals(actual.getTime(), expected.getTime(), ERROR_MESSAGE);
+    }
+  }
+
   @Test
   public void testHyperLogLog() {
     for (int i = 0; i < NUM_ITERATIONS; i++) {
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 53cd082fc0c..1687ab8196a 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
@@ -18,10 +18,19 @@
  */
 package org.apache.pinot.core.query.aggregation.function;
 
+import java.io.IOException;
+import java.util.UUID;
+import org.apache.pinot.common.datatable.DataTable;
+import org.apache.pinot.common.utils.DataSchema;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.core.common.datatable.DataTableBuilder;
+import org.apache.pinot.core.common.datatable.DataTableBuilderFactory;
 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.apache.pinot.spi.utils.ByteArray;
+import org.apache.pinot.spi.utils.UuidUtils;
 import org.testng.annotations.Test;
 
 import static org.mockito.Mockito.mock;
@@ -33,7 +42,8 @@ import static org.testng.Assert.fail;
 
 
 /// Unit test for {@link AggregationFunctionUtils#getAggregationResult}, the 
metadata/dictionary based aggregation
-/// result resolver used by the non-scan based and partial metadata based 
aggregation paths.
+/// result resolver used by the non-scan based and partial metadata based 
aggregation paths, and for
+/// {@link AggregationFunctionUtils#getConvertedFinalResult}.
 @SuppressWarnings("rawtypes")
 public class AggregationFunctionUtilsTest {
 
@@ -114,4 +124,17 @@ public class AggregationFunctionUtilsTest {
           () -> 
AggregationFunctionUtils.getAggregationResult(mockFunction(AggregationFunctionType.MIN),
 null, 100,
             "TEST"));
   }
+
+  @Test
+  public void testConvertedFinalResultForUuid()
+      throws IOException {
+    UUID uuid = UUID.randomUUID();
+    DataTableBuilder dataTableBuilder = 
DataTableBuilderFactory.getDataTableBuilder(
+        new DataSchema(new String[]{"uuid"}, new 
ColumnDataType[]{ColumnDataType.UUID}));
+    dataTableBuilder.startRow();
+    dataTableBuilder.setColumn(0, new ByteArray(UuidUtils.toBytes(uuid)));
+    dataTableBuilder.finishRow();
+    DataTable dataTable = dataTableBuilder.build();
+    assertEquals(AggregationFunctionUtils.getConvertedFinalResult(dataTable, 
ColumnDataType.UUID, 0, 0), uuid);
+  }
 }
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizerTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizerTest.java
new file mode 100644
index 00000000000..1051598be29
--- /dev/null
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/query/optimizer/statement/AggregateFunctionRewriteOptimizerTest.java
@@ -0,0 +1,147 @@
+/**
+ * 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.optimizer.statement;
+
+import org.apache.pinot.common.request.PinotQuery;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
+import org.apache.pinot.sql.parsers.CalciteSqlParser;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.expectThrows;
+
+
+/// Verifies which aggregation calls are rewritten from the schema, with and 
without the `autoRewriteAggregationType`
+/// query option. Each test compiles its own queries, so the class has no 
shared mutable state.
+public class AggregateFunctionRewriteOptimizerTest {
+  private static final Schema SCHEMA = new 
Schema.SchemaBuilder().setSchemaName("testTable")
+      .addSingleValueDimension("intCol", DataType.INT)
+      .addSingleValueDimension("longCol", DataType.LONG)
+      .addSingleValueDimension("floatCol", DataType.FLOAT)
+      .addSingleValueDimension("doubleCol", DataType.DOUBLE)
+      .addSingleValueDimension("bigDecimalCol", DataType.BIG_DECIMAL)
+      .addSingleValueDimension("stringCol", DataType.STRING)
+      .addSingleValueDimension("boolCol", DataType.BOOLEAN)
+      .addSingleValueDimension("timestampCol", DataType.TIMESTAMP)
+      .addSingleValueDimension("jsonCol", DataType.JSON)
+      .addSingleValueDimension("bytesCol", DataType.BYTES)
+      .addSingleValueDimension("uuidCol", DataType.UUID)
+      .addMultiValueDimension("mvCol", DataType.INT)
+      .addSingleValueDimension("timeCol", DataType.LONG)
+      .build();
+  private static final String AUTO_REWRITE = "SET 
autoRewriteAggregationType=true; ";
+
+  @DataProvider
+  public Object[][] dataTypes() {
+    return new Object[][]{
+        {"intCol", "INT"}, {"longCol", "LONG"}, {"floatCol", "FLOAT"}, 
{"doubleCol", "DOUBLE"},
+        {"bigDecimalCol", "BIG_DECIMAL"}, {"stringCol", "STRING"}, {"jsonCol", 
"STRING"}, {"boolCol", "BOOLEAN"},
+        {"timestampCol", "TIMESTAMP"}, {"bytesCol", "BYTES"}, {"uuidCol", 
"UUID"}
+    };
+  }
+
+  @Test(dataProvider = "dataTypes")
+  public void testValueWithTimeInfersDataType(String column, String dataType) {
+    for (String function : new String[]{"FIRST_WITH_TIME", "LASTWITHTIME"}) {
+      assertRewrite("SELECT " + function + "(" + column + ", timeCol) FROM 
testTable",
+          "SELECT " + function + "(" + column + ", timeCol, '" + dataType + 
"') FROM testTable");
+    }
+  }
+
+  @Test
+  public void testValueWithTimeRejectsMultiValueDataColumn() {
+    PinotQuery query = CalciteSqlParser.compileToPinotQuery("SELECT 
FIRST_WITH_TIME(mvCol, timeCol) FROM testTable");
+    BadQueryRequestException exception = 
expectThrows(BadQueryRequestException.class,
+        () -> new AggregateFunctionRewriteOptimizer().optimize(query, SCHEMA));
+    assertEquals(exception.getMessage(),
+        "Unsupported data type for FIRSTWITHTIME(dataColumn, timeColumn): 
multi-value INT");
+  }
+
+  @Test
+  public void testValueWithTimeKeepsDataColumnsWithoutSchemaType() {
+    // These keep the two-argument form, which fails with an error asking for 
the explicit data type.
+    for (String column : new String[]{"missingCol", "trim(stringCol)"}) {
+      assertUnchanged("SELECT FIRST_WITH_TIME(" + column + ", timeCol) FROM 
testTable");
+    }
+    assertUnchanged("SELECT FIRST_WITH_TIME(stringCol, timeCol, 'STRING'), 
LAST_WITH_TIME(intCol, timeCol, 'LONG') "
+        + "FROM testTable");
+  }
+
+  @Test
+  public void testRewritesNestedAggregationsConsistently() {
+    // Aliases, transforms, FILTER, HAVING and ORDER BY wrap the aggregation 
in another function.
+    assertRewrite("SELECT intCol, FIRST_WITH_TIME(stringCol, timeCol) AS v1, "
+            + "trim(LAST_WITH_TIME(stringCol, timeCol)) AS v2, "
+            + "FIRST_WITH_TIME(doubleCol, timeCol) FILTER (WHERE boolCol) "
+            + "FROM testTable GROUP BY intCol HAVING FIRST_WITH_TIME(longCol, 
timeCol) > 1 "
+            + "ORDER BY LAST_WITH_TIME(timestampCol, timeCol)",
+        "SELECT intCol, FIRST_WITH_TIME(stringCol, timeCol, 'STRING') AS v1, "
+            + "trim(LAST_WITH_TIME(stringCol, timeCol, 'STRING')) AS v2, "
+            + "FIRST_WITH_TIME(doubleCol, timeCol, 'DOUBLE') FILTER (WHERE 
boolCol) "
+            + "FROM testTable GROUP BY intCol HAVING FIRST_WITH_TIME(longCol, 
timeCol, 'LONG') > 1 "
+            + "ORDER BY LAST_WITH_TIME(timestampCol, timeCol, 'TIMESTAMP')");
+  }
+
+  @Test
+  public void testTypedVariantsRequireQueryOption() {
+    String query = "SELECT MIN(stringCol) AS m, MAX(longCol), SUM(intCol), 
SUM(longCol), MIN(doubleCol) "
+        + "FROM testTable ORDER BY MIN(stringCol)";
+    assertUnchanged(query);
+    assertRewrite(AUTO_REWRITE + query, AUTO_REWRITE
+        + "SELECT MINSTRING(stringCol) AS m, MAXLONG(longCol), SUMINT(intCol), 
SUMLONG(longCol), MIN(doubleCol) "
+        + "FROM testTable ORDER BY MINSTRING(stringCol)");
+  }
+
+  @Test
+  public void testTypedVariantsRewriteNestedAggregations() {
+    // Aggregations under an alias, a transform and HAVING are rewritten like 
top-level ones, so the select list and
+    // HAVING compute the same aggregation.
+    String query = "SELECT intCol, MIN(longCol) AS m, MAX(longCol) + 1 FROM 
testTable GROUP BY intCol "
+        + "HAVING MIN(longCol) > 5";
+    assertUnchanged(query);
+    assertRewrite(AUTO_REWRITE + query, AUTO_REWRITE
+        + "SELECT intCol, MINLONG(longCol) AS m, MAXLONG(longCol) + 1 FROM 
testTable GROUP BY intCol "
+        + "HAVING MINLONG(longCol) > 5");
+  }
+
+  @Test
+  public void testNoSchema() {
+    PinotQuery query =
+        CalciteSqlParser.compileToPinotQuery("SELECT 
FIRST_WITH_TIME(stringCol, timeCol) FROM testTable");
+    PinotQuery original = query.deepCopy();
+    new AggregateFunctionRewriteOptimizer().optimize(query, null);
+    assertEquals(query, original);
+  }
+
+  private static void assertRewrite(String query, String expectedQuery) {
+    PinotQuery actual = CalciteSqlParser.compileToPinotQuery(query);
+    new AggregateFunctionRewriteOptimizer().optimize(actual, SCHEMA);
+    PinotQuery expected = CalciteSqlParser.compileToPinotQuery(expectedQuery);
+    assertEquals(actual.getSelectList(), expected.getSelectList());
+    assertEquals(actual.getHavingExpression(), expected.getHavingExpression());
+    assertEquals(actual.getOrderByList(), expected.getOrderByList());
+  }
+
+  private static void assertUnchanged(String query) {
+    assertRewrite(query, query);
+  }
+}
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/queries/FirstWithTimeQueriesTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/queries/FirstWithTimeQueriesTest.java
index 768160e1fef..cf1277a0f16 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/queries/FirstWithTimeQueriesTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/queries/FirstWithTimeQueriesTest.java
@@ -21,6 +21,8 @@ package org.apache.pinot.queries;
 import com.google.common.collect.Lists;
 import java.io.File;
 import java.io.IOException;
+import java.math.BigDecimal;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -28,6 +30,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import java.util.UUID;
 import org.apache.commons.io.FileUtils;
 import org.apache.pinot.common.response.broker.BrokerResponseNative;
 import org.apache.pinot.common.response.broker.ResultTable;
@@ -55,6 +58,7 @@ import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.utils.BytesUtils;
 import org.apache.pinot.spi.utils.ReadMode;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.testng.annotations.AfterClass;
@@ -90,6 +94,10 @@ public class FirstWithTimeQueriesTest extends 
BaseQueriesTest {
   private static final String STRING_COLUMN = "stringColumn";
   private static final String STRING_NO_DICT_COLUMN = "stringNoDictColumn";
   private static final String TIME_COLUMN = "timestampColumn";
+  private static final String TIMESTAMP_VALUE_COLUMN = "timestampValueColumn";
+  private static final String BIG_DECIMAL_COLUMN = "bigDecimalColumn";
+  private static final String BYTES_COLUMN = "bytesColumn";
+  private static final String UUID_COLUMN = "uuidColumn";
   private static final Schema SCHEMA = new Schema.SchemaBuilder()
       .addSingleValueDimension(BOOL_COLUMN, DataType.BOOLEAN)
       .addSingleValueDimension(BOOL_NO_DICT_COLUMN, DataType.BOOLEAN)
@@ -104,6 +112,10 @@ public class FirstWithTimeQueriesTest extends 
BaseQueriesTest {
       .addSingleValueDimension(DOUBLE_NO_DICT_COLUMN, DataType.DOUBLE)
       .addSingleValueDimension(STRING_COLUMN, DataType.STRING)
       .addSingleValueDimension(STRING_NO_DICT_COLUMN, DataType.STRING)
+      .addSingleValueDimension(TIMESTAMP_VALUE_COLUMN, DataType.TIMESTAMP)
+      .addSingleValueDimension(BYTES_COLUMN, DataType.BYTES)
+      .addSingleValueDimension(UUID_COLUMN, DataType.UUID)
+      .addMetric(BIG_DECIMAL_COLUMN, DataType.BIG_DECIMAL)
       .addSingleValueDimension(TIME_COLUMN, DataType.LONG).build();
   private static final TableConfig TABLE_CONFIG = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(RAW_TABLE_NAME)
       .setNoDictionaryColumns(
@@ -116,12 +128,20 @@ public class FirstWithTimeQueriesTest extends 
BaseQueriesTest {
   private Float _expectedResultFirstFloat;
   private Double _expectedResultFirstDouble;
   private String _expectedResultFirstString;
+  private Long _expectedResultFirstTimestamp;
+  private BigDecimal _expectedResultFirstBigDecimal;
+  private byte[] _expectedResultFirstBytes;
+  private String _expectedResultFirstUuid;
   private Map<Integer, Boolean> _boolGroupValues;
   private Map<Integer, Integer> _intGroupValues;
   private Map<Integer, Long> _longGroupValues;
   private Map<Integer, Float> _floatGroupValues;
   private Map<Integer, Double> _doubleGroupValues;
   private Map<Integer, String> _stringGroupValues;
+  private Map<Integer, Long> _timestampGroupValues;
+  private Map<Integer, BigDecimal> _bigDecimalGroupValues;
+  private Map<Integer, byte[]> _bytesGroupValues;
+  private Map<Integer, String> _uuidGroupValues;
   private IndexSegment _indexSegment;
   private List<IndexSegment> _indexSegments;
 
@@ -152,6 +172,10 @@ public class FirstWithTimeQueriesTest extends 
BaseQueriesTest {
     _floatGroupValues = new HashMap<>();
     _doubleGroupValues = new HashMap<>();
     _stringGroupValues = new HashMap<>();
+    _timestampGroupValues = new HashMap<>();
+    _bigDecimalGroupValues = new HashMap<>();
+    _bytesGroupValues = new HashMap<>();
+    _uuidGroupValues = new HashMap<>();
     for (int i = 0; i < NUM_RECORDS; i++) {
       boolean boolValue = RANDOM.nextBoolean();
       int intValue = RANDOM.nextInt(MAX_VALUE);
@@ -159,6 +183,11 @@ public class FirstWithTimeQueriesTest extends 
BaseQueriesTest {
       float floatValue = RANDOM.nextFloat();
       double doubleValue = RANDOM.nextDouble();
       String strValue = String.valueOf(RANDOM.nextDouble());
+      long timestampValue = 1_600_000_000_000L + RANDOM.nextInt(1_000_000_000);
+      BigDecimal bigDecimalValue = BigDecimal.valueOf(RANDOM.nextLong(), 3);
+      byte[] bytesValue = new byte[4];
+      RANDOM.nextBytes(bytesValue);
+      String uuidValue = new UUID(RANDOM.nextLong(), 
RANDOM.nextLong()).toString();
       GenericRow record = new GenericRow();
       record.putValue(BOOL_COLUMN, boolValue);
       record.putValue(BOOL_NO_DICT_COLUMN, boolValue);
@@ -173,6 +202,10 @@ public class FirstWithTimeQueriesTest extends 
BaseQueriesTest {
       record.putValue(DOUBLE_NO_DICT_COLUMN, doubleValue);
       record.putValue(STRING_COLUMN, strValue);
       record.putValue(STRING_NO_DICT_COLUMN, strValue);
+      record.putValue(TIMESTAMP_VALUE_COLUMN, timestampValue);
+      record.putValue(BIG_DECIMAL_COLUMN, bigDecimalValue);
+      record.putValue(BYTES_COLUMN, bytesValue);
+      record.putValue(UUID_COLUMN, uuidValue);
       record.putValue(TIME_COLUMN, (long) i);
       if (i == 0) {
         _expectedResultFirstBoolean = boolValue;
@@ -181,6 +214,10 @@ public class FirstWithTimeQueriesTest extends 
BaseQueriesTest {
         _expectedResultFirstFloat = floatValue;
         _expectedResultFirstDouble = doubleValue;
         _expectedResultFirstString = strValue;
+        _expectedResultFirstTimestamp = timestampValue;
+        _expectedResultFirstBigDecimal = bigDecimalValue;
+        _expectedResultFirstBytes = bytesValue;
+        _expectedResultFirstUuid = uuidValue;
       }
       _boolGroupValues.putIfAbsent(intValue, boolValue);
       _intGroupValues.putIfAbsent(intValue, intValue);
@@ -188,6 +225,10 @@ public class FirstWithTimeQueriesTest extends 
BaseQueriesTest {
       _floatGroupValues.putIfAbsent(intValue, floatValue);
       _doubleGroupValues.putIfAbsent(intValue, doubleValue);
       _stringGroupValues.putIfAbsent(intValue, strValue);
+      _timestampGroupValues.putIfAbsent(intValue, timestampValue);
+      _bigDecimalGroupValues.putIfAbsent(intValue, bigDecimalValue);
+      _bytesGroupValues.putIfAbsent(intValue, bytesValue);
+      _uuidGroupValues.putIfAbsent(intValue, uuidValue);
       records.add(record);
     }
 
@@ -423,6 +464,113 @@ public class FirstWithTimeQueriesTest extends 
BaseQueriesTest {
     }
   }
 
+  /// The data type argument can be omitted for a column; the optimizer 
supplies it from the schema.
+  @Test
+  public void testAggregationOnlyWithInferredDataType() {
+    String query = "SELECT "
+        + "FIRSTWITHTIME(boolColumn, timestampColumn), "
+        + "FIRSTWITHTIME(intColumn, timestampColumn), "
+        + "FIRSTWITHTIME(longColumn, timestampColumn), "
+        + "FIRSTWITHTIME(floatColumn, timestampColumn), "
+        + "FIRSTWITHTIME(doubleColumn, timestampColumn), "
+        + "FIRSTWITHTIME(stringColumn, timestampColumn), "
+        + "FIRSTWITHTIME(timestampValueColumn, timestampColumn), "
+        + "FIRSTWITHTIME(timestampValueColumn, timestampColumn, 'TIMESTAMP'), "
+        + "FIRSTWITHTIME(bigDecimalColumn, timestampColumn), "
+        + "FIRSTWITHTIME(bytesColumn, timestampColumn), "
+        + "FIRSTWITHTIME(uuidColumn, timestampColumn) "
+        + "FROM testTable";
+    BrokerResponseNative brokerResponse = 
getBrokerResponseForOptimizedQuery(query, SCHEMA);
+    // The result columns are named after the explicit form the query is 
rewritten to.
+    DataSchema expectedDataSchema = new DataSchema(new String[]{
+        "firstwithtime(boolColumn,timestampColumn,'BOOLEAN')",
+        "firstwithtime(intColumn,timestampColumn,'INT')",
+        "firstwithtime(longColumn,timestampColumn,'LONG')",
+        "firstwithtime(floatColumn,timestampColumn,'FLOAT')",
+        "firstwithtime(doubleColumn,timestampColumn,'DOUBLE')",
+        "firstwithtime(stringColumn,timestampColumn,'STRING')",
+        "firstwithtime(timestampValueColumn,timestampColumn,'TIMESTAMP')",
+        "firstwithtime(timestampValueColumn,timestampColumn,'TIMESTAMP')",
+        "firstwithtime(bigDecimalColumn,timestampColumn,'BIG_DECIMAL')",
+        "firstwithtime(bytesColumn,timestampColumn,'BYTES')",
+        "firstwithtime(uuidColumn,timestampColumn,'UUID')"
+    }, new ColumnDataType[]{
+        ColumnDataType.BOOLEAN,
+        ColumnDataType.INT,
+        ColumnDataType.LONG,
+        ColumnDataType.FLOAT,
+        ColumnDataType.DOUBLE,
+        ColumnDataType.STRING,
+        ColumnDataType.TIMESTAMP,
+        ColumnDataType.TIMESTAMP,
+        ColumnDataType.BIG_DECIMAL,
+        ColumnDataType.BYTES,
+        ColumnDataType.UUID
+    });
+    String expectedTimestamp = new 
Timestamp(_expectedResultFirstTimestamp).toString();
+    Object[] expectedResults = new Object[]{
+        _expectedResultFirstBoolean,
+        _expectedResultFirstInt,
+        _expectedResultFirstLong,
+        _expectedResultFirstFloat,
+        _expectedResultFirstDouble,
+        _expectedResultFirstString,
+        expectedTimestamp,
+        expectedTimestamp,
+        // Ingestion strips trailing zeros from BIG_DECIMAL values.
+        _expectedResultFirstBigDecimal.stripTrailingZeros().toPlainString(),
+        BytesUtils.toHexString(_expectedResultFirstBytes),
+        _expectedResultFirstUuid
+    };
+    QueriesTestUtils.testInterSegmentsResult(brokerResponse, 4 * NUM_RECORDS, 
0L, 4 * 11 * NUM_RECORDS,
+        4 * NUM_RECORDS, new ResultTable(expectedDataSchema, 
List.<Object[]>of(expectedResults)));
+  }
+
+  @Test
+  public void testAggregationOnlyWithInferredDataTypeAndNoMatchingRows() {
+    // Without matching rows, the results are the default values; for UUID it 
is the nil UUID.
+    String query = "SELECT FIRSTWITHTIME(bigDecimalColumn, timestampColumn), "
+        + "FIRSTWITHTIME(bytesColumn, timestampColumn), 
FIRSTWITHTIME(uuidColumn, timestampColumn) "
+        + "FROM testTable WHERE timestampColumn < 0";
+    ResultTable resultTable = getBrokerResponseForOptimizedQuery(query, 
SCHEMA).getResultTable();
+    assertEquals(resultTable.getRows().get(0), new Object[]{"0", "", 
"00000000-0000-0000-0000-000000000000"});
+  }
+
+  @Test
+  public void testAggregationGroupByWithInferredDataType() {
+    String query = "SELECT intColumn AS key, "
+        + "FIRSTWITHTIME(boolColumn, timestampColumn) AS v1, "
+        + "FIRSTWITHTIME(longNoDictColumn, timestampColumn) AS v2, "
+        + "FIRSTWITHTIME(stringColumn, timestampColumn) AS v3, "
+        + "FIRSTWITHTIME(timestampValueColumn, timestampColumn) AS v4, "
+        + "FIRSTWITHTIME(bytesColumn, timestampColumn) AS v5, "
+        + "FIRSTWITHTIME(uuidColumn, timestampColumn) AS v6, "
+        + "FIRSTWITHTIME(bigDecimalColumn, timestampColumn) AS v7 "
+        + "FROM testTable GROUP BY key ORDER BY 
FIRSTWITHTIME(longNoDictColumn, timestampColumn)";
+    ResultTable resultTable = getBrokerResponseForOptimizedQuery(query, 
SCHEMA).getResultTable();
+    assertEquals(resultTable.getDataSchema(),
+        new DataSchema(new String[]{"key", "v1", "v2", "v3", "v4", "v5", "v6", 
"v7"}, new ColumnDataType[]{
+            ColumnDataType.INT, ColumnDataType.BOOLEAN, ColumnDataType.LONG, 
ColumnDataType.STRING,
+            ColumnDataType.TIMESTAMP, ColumnDataType.BYTES, 
ColumnDataType.UUID, ColumnDataType.BIG_DECIMAL
+        }));
+    List<Object[]> rows = resultTable.getRows();
+    assertEquals(rows.size(), 10);
+    long previous = Long.MIN_VALUE;
+    for (Object[] row : rows) {
+      int key = (Integer) row[0];
+      assertEquals(row[1], _boolGroupValues.get(key));
+      assertEquals(row[2], _longGroupValues.get(key));
+      assertEquals(row[3], _stringGroupValues.get(key));
+      assertEquals(row[4], new 
Timestamp(_timestampGroupValues.get(key)).toString());
+      assertEquals(row[5], BytesUtils.toHexString(_bytesGroupValues.get(key)));
+      assertEquals(row[6], _uuidGroupValues.get(key));
+      assertEquals(row[7], 
_bigDecimalGroupValues.get(key).stripTrailingZeros().toPlainString());
+      // ORDER BY repeats the call, so it must be rewritten the same way as 
the SELECT list.
+      assertTrue((Long) row[2] >= previous);
+      previous = (Long) row[2];
+    }
+  }
+
   @AfterClass
   public void tearDown()
       throws IOException {
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/queries/LastWithTimeQueriesTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/queries/LastWithTimeQueriesTest.java
index 2dd14e5455e..dbc8ba0714f 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/queries/LastWithTimeQueriesTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/queries/LastWithTimeQueriesTest.java
@@ -21,6 +21,8 @@ package org.apache.pinot.queries;
 import com.google.common.collect.Lists;
 import java.io.File;
 import java.io.IOException;
+import java.math.BigDecimal;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -28,6 +30,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import java.util.UUID;
 import org.apache.commons.io.FileUtils;
 import org.apache.pinot.common.response.broker.BrokerResponseNative;
 import org.apache.pinot.common.response.broker.ResultTable;
@@ -55,6 +58,7 @@ import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.utils.BytesUtils;
 import org.apache.pinot.spi.utils.ReadMode;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.testng.annotations.AfterClass;
@@ -90,6 +94,10 @@ public class LastWithTimeQueriesTest extends BaseQueriesTest 
{
   private static final String STRING_COLUMN = "stringColumn";
   private static final String STRING_NO_DICT_COLUMN = "stringNoDictColumn";
   private static final String TIME_COLUMN = "timestampColumn";
+  private static final String TIMESTAMP_VALUE_COLUMN = "timestampValueColumn";
+  private static final String BIG_DECIMAL_COLUMN = "bigDecimalColumn";
+  private static final String BYTES_COLUMN = "bytesColumn";
+  private static final String UUID_COLUMN = "uuidColumn";
   private static final Schema SCHEMA = new Schema.SchemaBuilder()
       .addSingleValueDimension(BOOL_COLUMN, DataType.BOOLEAN)
       .addSingleValueDimension(BOOL_NO_DICT_COLUMN, DataType.BOOLEAN)
@@ -104,6 +112,10 @@ public class LastWithTimeQueriesTest extends 
BaseQueriesTest {
       .addSingleValueDimension(DOUBLE_NO_DICT_COLUMN, DataType.DOUBLE)
       .addSingleValueDimension(STRING_COLUMN, DataType.STRING)
       .addSingleValueDimension(STRING_NO_DICT_COLUMN, DataType.STRING)
+      .addSingleValueDimension(TIMESTAMP_VALUE_COLUMN, DataType.TIMESTAMP)
+      .addSingleValueDimension(BYTES_COLUMN, DataType.BYTES)
+      .addSingleValueDimension(UUID_COLUMN, DataType.UUID)
+      .addMetric(BIG_DECIMAL_COLUMN, DataType.BIG_DECIMAL)
       .addSingleValueDimension(TIME_COLUMN, DataType.LONG).build();
   private static final TableConfig TABLE_CONFIG = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(RAW_TABLE_NAME)
       .setNoDictionaryColumns(
@@ -116,12 +128,20 @@ public class LastWithTimeQueriesTest extends 
BaseQueriesTest {
   private Float _expectedResultLastFloat;
   private Double _expectedResultLastDouble;
   private String _expectedResultLastString;
+  private Long _expectedResultLastTimestamp;
+  private BigDecimal _expectedResultLastBigDecimal;
+  private byte[] _expectedResultLastBytes;
+  private String _expectedResultLastUuid;
   private Map<Integer, Boolean> _boolGroupValues;
   private Map<Integer, Integer> _intGroupValues;
   private Map<Integer, Long> _longGroupValues;
   private Map<Integer, Float> _floatGroupValues;
   private Map<Integer, Double> _doubleGroupValues;
   private Map<Integer, String> _stringGroupValues;
+  private Map<Integer, Long> _timestampGroupValues;
+  private Map<Integer, BigDecimal> _bigDecimalGroupValues;
+  private Map<Integer, byte[]> _bytesGroupValues;
+  private Map<Integer, String> _uuidGroupValues;
   private IndexSegment _indexSegment;
   private List<IndexSegment> _indexSegments;
 
@@ -152,6 +172,10 @@ public class LastWithTimeQueriesTest extends 
BaseQueriesTest {
     _floatGroupValues = new HashMap<>();
     _doubleGroupValues = new HashMap<>();
     _stringGroupValues = new HashMap<>();
+    _timestampGroupValues = new HashMap<>();
+    _bigDecimalGroupValues = new HashMap<>();
+    _bytesGroupValues = new HashMap<>();
+    _uuidGroupValues = new HashMap<>();
     for (int i = 0; i < NUM_RECORDS; i++) {
       boolean boolValue = RANDOM.nextBoolean();
       int intValue = RANDOM.nextInt(MAX_VALUE);
@@ -159,6 +183,11 @@ public class LastWithTimeQueriesTest extends 
BaseQueriesTest {
       float floatValue = RANDOM.nextFloat();
       double doubleValue = RANDOM.nextDouble();
       String strValue = String.valueOf(RANDOM.nextDouble());
+      long timestampValue = 1_600_000_000_000L + RANDOM.nextInt(1_000_000_000);
+      BigDecimal bigDecimalValue = BigDecimal.valueOf(RANDOM.nextLong(), 3);
+      byte[] bytesValue = new byte[4];
+      RANDOM.nextBytes(bytesValue);
+      String uuidValue = new UUID(RANDOM.nextLong(), 
RANDOM.nextLong()).toString();
       GenericRow record = new GenericRow();
       record.putValue(BOOL_COLUMN, boolValue);
       record.putValue(BOOL_NO_DICT_COLUMN, boolValue);
@@ -173,6 +202,10 @@ public class LastWithTimeQueriesTest extends 
BaseQueriesTest {
       record.putValue(DOUBLE_NO_DICT_COLUMN, doubleValue);
       record.putValue(STRING_COLUMN, strValue);
       record.putValue(STRING_NO_DICT_COLUMN, strValue);
+      record.putValue(TIMESTAMP_VALUE_COLUMN, timestampValue);
+      record.putValue(BIG_DECIMAL_COLUMN, bigDecimalValue);
+      record.putValue(BYTES_COLUMN, bytesValue);
+      record.putValue(UUID_COLUMN, uuidValue);
       record.putValue(TIME_COLUMN, (long) i);
       if (i == NUM_RECORDS - 1) {
         _expectedResultLastBoolean = boolValue;
@@ -181,6 +214,10 @@ public class LastWithTimeQueriesTest extends 
BaseQueriesTest {
         _expectedResultLastFloat = floatValue;
         _expectedResultLastDouble = doubleValue;
         _expectedResultLastString = strValue;
+        _expectedResultLastTimestamp = timestampValue;
+        _expectedResultLastBigDecimal = bigDecimalValue;
+        _expectedResultLastBytes = bytesValue;
+        _expectedResultLastUuid = uuidValue;
       }
       _boolGroupValues.put(intValue, boolValue);
       _intGroupValues.put(intValue, intValue);
@@ -188,6 +225,10 @@ public class LastWithTimeQueriesTest extends 
BaseQueriesTest {
       _floatGroupValues.put(intValue, floatValue);
       _doubleGroupValues.put(intValue, doubleValue);
       _stringGroupValues.put(intValue, strValue);
+      _timestampGroupValues.put(intValue, timestampValue);
+      _bigDecimalGroupValues.put(intValue, bigDecimalValue);
+      _bytesGroupValues.put(intValue, bytesValue);
+      _uuidGroupValues.put(intValue, uuidValue);
       records.add(record);
     }
 
@@ -423,6 +464,113 @@ public class LastWithTimeQueriesTest extends 
BaseQueriesTest {
     }
   }
 
+  /// The data type argument can be omitted for a column; the optimizer 
supplies it from the schema.
+  @Test
+  public void testAggregationOnlyWithInferredDataType() {
+    String query = "SELECT "
+        + "LASTWITHTIME(boolColumn, timestampColumn), "
+        + "LASTWITHTIME(intColumn, timestampColumn), "
+        + "LASTWITHTIME(longColumn, timestampColumn), "
+        + "LASTWITHTIME(floatColumn, timestampColumn), "
+        + "LASTWITHTIME(doubleColumn, timestampColumn), "
+        + "LASTWITHTIME(stringColumn, timestampColumn), "
+        + "LASTWITHTIME(timestampValueColumn, timestampColumn), "
+        + "LASTWITHTIME(timestampValueColumn, timestampColumn, 'TIMESTAMP'), "
+        + "LASTWITHTIME(bigDecimalColumn, timestampColumn), "
+        + "LASTWITHTIME(bytesColumn, timestampColumn), "
+        + "LASTWITHTIME(uuidColumn, timestampColumn) "
+        + "FROM testTable";
+    BrokerResponseNative brokerResponse = 
getBrokerResponseForOptimizedQuery(query, SCHEMA);
+    // The result columns are named after the explicit form the query is 
rewritten to.
+    DataSchema expectedDataSchema = new DataSchema(new String[]{
+        "lastwithtime(boolColumn,timestampColumn,'BOOLEAN')",
+        "lastwithtime(intColumn,timestampColumn,'INT')",
+        "lastwithtime(longColumn,timestampColumn,'LONG')",
+        "lastwithtime(floatColumn,timestampColumn,'FLOAT')",
+        "lastwithtime(doubleColumn,timestampColumn,'DOUBLE')",
+        "lastwithtime(stringColumn,timestampColumn,'STRING')",
+        "lastwithtime(timestampValueColumn,timestampColumn,'TIMESTAMP')",
+        "lastwithtime(timestampValueColumn,timestampColumn,'TIMESTAMP')",
+        "lastwithtime(bigDecimalColumn,timestampColumn,'BIG_DECIMAL')",
+        "lastwithtime(bytesColumn,timestampColumn,'BYTES')",
+        "lastwithtime(uuidColumn,timestampColumn,'UUID')"
+    }, new ColumnDataType[]{
+        ColumnDataType.BOOLEAN,
+        ColumnDataType.INT,
+        ColumnDataType.LONG,
+        ColumnDataType.FLOAT,
+        ColumnDataType.DOUBLE,
+        ColumnDataType.STRING,
+        ColumnDataType.TIMESTAMP,
+        ColumnDataType.TIMESTAMP,
+        ColumnDataType.BIG_DECIMAL,
+        ColumnDataType.BYTES,
+        ColumnDataType.UUID
+    });
+    String expectedTimestamp = new 
Timestamp(_expectedResultLastTimestamp).toString();
+    Object[] expectedResults = new Object[]{
+        _expectedResultLastBoolean,
+        _expectedResultLastInt,
+        _expectedResultLastLong,
+        _expectedResultLastFloat,
+        _expectedResultLastDouble,
+        _expectedResultLastString,
+        expectedTimestamp,
+        expectedTimestamp,
+        // Ingestion strips trailing zeros from BIG_DECIMAL values.
+        _expectedResultLastBigDecimal.stripTrailingZeros().toPlainString(),
+        BytesUtils.toHexString(_expectedResultLastBytes),
+        _expectedResultLastUuid
+    };
+    QueriesTestUtils.testInterSegmentsResult(brokerResponse, 4 * NUM_RECORDS, 
0L, 4 * 11 * NUM_RECORDS,
+        4 * NUM_RECORDS, new ResultTable(expectedDataSchema, 
List.<Object[]>of(expectedResults)));
+  }
+
+  @Test
+  public void testAggregationOnlyWithInferredDataTypeAndNoMatchingRows() {
+    // Without matching rows, the results are the default values; for UUID it 
is the nil UUID.
+    String query = "SELECT LASTWITHTIME(bigDecimalColumn, timestampColumn), "
+        + "LASTWITHTIME(bytesColumn, timestampColumn), 
LASTWITHTIME(uuidColumn, timestampColumn) "
+        + "FROM testTable WHERE timestampColumn < 0";
+    ResultTable resultTable = getBrokerResponseForOptimizedQuery(query, 
SCHEMA).getResultTable();
+    assertEquals(resultTable.getRows().get(0), new Object[]{"0", "", 
"00000000-0000-0000-0000-000000000000"});
+  }
+
+  @Test
+  public void testAggregationGroupByWithInferredDataType() {
+    String query = "SELECT intColumn AS key, "
+        + "LASTWITHTIME(boolColumn, timestampColumn) AS v1, "
+        + "LASTWITHTIME(longNoDictColumn, timestampColumn) AS v2, "
+        + "LASTWITHTIME(stringColumn, timestampColumn) AS v3, "
+        + "LASTWITHTIME(timestampValueColumn, timestampColumn) AS v4, "
+        + "LASTWITHTIME(bytesColumn, timestampColumn) AS v5, "
+        + "LASTWITHTIME(uuidColumn, timestampColumn) AS v6, "
+        + "LASTWITHTIME(bigDecimalColumn, timestampColumn) AS v7 "
+        + "FROM testTable GROUP BY key ORDER BY LASTWITHTIME(longNoDictColumn, 
timestampColumn)";
+    ResultTable resultTable = getBrokerResponseForOptimizedQuery(query, 
SCHEMA).getResultTable();
+    assertEquals(resultTable.getDataSchema(),
+        new DataSchema(new String[]{"key", "v1", "v2", "v3", "v4", "v5", "v6", 
"v7"}, new ColumnDataType[]{
+            ColumnDataType.INT, ColumnDataType.BOOLEAN, ColumnDataType.LONG, 
ColumnDataType.STRING,
+            ColumnDataType.TIMESTAMP, ColumnDataType.BYTES, 
ColumnDataType.UUID, ColumnDataType.BIG_DECIMAL
+        }));
+    List<Object[]> rows = resultTable.getRows();
+    assertEquals(rows.size(), 10);
+    long previous = Long.MIN_VALUE;
+    for (Object[] row : rows) {
+      int key = (Integer) row[0];
+      assertEquals(row[1], _boolGroupValues.get(key));
+      assertEquals(row[2], _longGroupValues.get(key));
+      assertEquals(row[3], _stringGroupValues.get(key));
+      assertEquals(row[4], new 
Timestamp(_timestampGroupValues.get(key)).toString());
+      assertEquals(row[5], BytesUtils.toHexString(_bytesGroupValues.get(key)));
+      assertEquals(row[6], _uuidGroupValues.get(key));
+      assertEquals(row[7], 
_bigDecimalGroupValues.get(key).stripTrailingZeros().toPlainString());
+      // ORDER BY repeats the call, so it must be rewritten the same way as 
the SELECT list.
+      assertTrue((Long) row[2] >= previous);
+      previous = (Long) row[2];
+    }
+  }
+
   @AfterClass
   public void tearDown()
       throws IOException {
diff --git 
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateTypeArgumentRewriteRule.java
 
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateTypeArgumentRewriteRule.java
new file mode 100644
index 00000000000..aa739af7230
--- /dev/null
+++ 
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateTypeArgumentRewriteRule.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.calcite.rel.rules;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.hint.RelHint;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.pinot.segment.spi.AggregationFunctionType;
+
+
+/// Supplies the explicit data type argument from the type of the data 
argument:
+/// - FIRST_WITH_TIME(dataColumn, timeColumn) -> FIRST_WITH_TIME(dataColumn, 
timeColumn, 'dataType')
+/// - LAST_WITH_TIME(dataColumn, timeColumn) -> LAST_WITH_TIME(dataColumn, 
timeColumn, 'dataType')
+///
+/// The result type of the call is unchanged. The literal is projected 
directly beneath the aggregate, appended to the
+/// existing project when there is one, because 
[PinotAggregateExchangeNodeInsertRule] and `AggregatePushdownRule`
+/// inline literal arguments only from the immediate project.
+///
+/// The servers require the data type argument, so the rule is required rather 
than an optimization: `QueryEnvironment`
+/// always applies it, and `skipPlannerRules` cannot turn it off.
+public class PinotAggregateTypeArgumentRewriteRule extends RelOptRule {
+  public static final PinotAggregateTypeArgumentRewriteRule INSTANCE =
+      new 
PinotAggregateTypeArgumentRewriteRule(PinotRuleUtils.PINOT_REL_FACTORY);
+
+  // Matches with RelOptRule operands, like the other aggregate rewrite rules, 
instead of a RelRule config.
+  @SuppressWarnings("deprecation")
+  private PinotAggregateTypeArgumentRewriteRule(RelBuilderFactory factory) {
+    super(operand(LogicalAggregate.class, any()), factory, null);
+  }
+
+  @Override
+  public boolean matches(RelOptRuleCall call) {
+    // Requiring a call without its data type is also what makes the rule 
terminate.
+    Aggregate aggRel = call.rel(0);
+    RelNode input = aggRel.getInput();
+    return aggRel.getAggCallList().stream().anyMatch(aggCall -> 
getDataTypeArgument(aggCall, input) != null);
+  }
+
+  @Override
+  public void onMatch(RelOptRuleCall call) {
+    Aggregate aggRel = call.rel(0);
+    RelNode input = aggRel.getInput();
+    RelNode unboxedInput = PinotRuleUtils.unboxRel(input);
+    RexBuilder rexBuilder = aggRel.getCluster().getRexBuilder();
+
+    List<RexNode> projects;
+    RelNode projectInput;
+    List<RelHint> hints;
+    if (unboxedInput instanceof Project) {
+      Project project = (Project) unboxedInput;
+      projects = new ArrayList<>(project.getProjects());
+      projectInput = project.getInput();
+      hints = project.getHints();
+    } else {
+      int numFields = input.getRowType().getFieldCount();
+      projects = new ArrayList<>(numFields + 1);
+      for (int i = 0; i < numFields; i++) {
+        projects.add(rexBuilder.makeInputRef(input, i));
+      }
+      projectInput = input;
+      hints = List.of();
+    }
+
+    // Appending keeps the existing field indexes, so the group set and the 
other calls are unaffected.
+    List<AggregateCall> orgAggCalls = aggRel.getAggCallList();
+    List<AggregateCall> aggCalls = new ArrayList<>(orgAggCalls.size());
+    Map<String, Integer> literalIndexByDataType = new HashMap<>();
+    for (AggregateCall aggCall : orgAggCalls) {
+      String dataType = getDataTypeArgument(aggCall, input);
+      if (dataType == null) {
+        aggCalls.add(aggCall);
+        continue;
+      }
+      List<Integer> argList = new ArrayList<>(aggCall.getArgList());
+      argList.add(literalIndexByDataType.computeIfAbsent(dataType, type -> {
+        projects.add(rexBuilder.makeLiteral(type));
+        return projects.size() - 1;
+      }));
+      // Keeps the original result type, so the rewrite does not change the 
result schema.
+      aggCalls.add(aggCall.withArgList(argList));
+    }
+    RelNode newInput = LogicalProject.create(projectInput, hints, projects, 
(List<String>) null, Set.of());
+    call.transformTo(
+        aggRel.copy(aggRel.getTraitSet(), newInput, aggRel.getGroupSet(), 
aggRel.getGroupSets(), aggCalls));
+  }
+
+  /// Returns the data type argument to append, or `null` when the call 
already has one or is not such a call. Throws
+  /// for a data argument without an explicit form, such as an array from a 
multi-value column.
+  @Nullable
+  private static String getDataTypeArgument(AggregateCall aggCall, RelNode 
input) {
+    if (aggCall.getArgList().size() != 2) {
+      return null;
+    }
+    String name = 
AggregationFunctionType.getNormalizedAggregationFunctionName(aggCall.getAggregation().getName());
+    if (!name.equals(AggregationFunctionType.FIRSTWITHTIME.name())
+        && !name.equals(AggregationFunctionType.LASTWITHTIME.name())) {
+      return null;
+    }
+    SqlTypeName dataType =
+        
input.getRowType().getFieldList().get(aggCall.getArgList().get(0)).getType().getSqlTypeName();
+    switch (dataType) {
+      case TINYINT:
+      case SMALLINT:
+      case INTEGER:
+        return "INT";
+      case BIGINT:
+        return "LONG";
+      case REAL:
+      case FLOAT:
+        return "FLOAT";
+      case DOUBLE:
+        return "DOUBLE";
+      case DECIMAL:
+        return "BIG_DECIMAL";
+      case CHAR:
+      case VARCHAR:
+        return "STRING";
+      case BOOLEAN:
+        return "BOOLEAN";
+      case TIMESTAMP:
+        return "TIMESTAMP";
+      case BINARY:
+      case VARBINARY:
+        return "BYTES";
+      case UUID:
+        return "UUID";
+      default:
+        throw new IllegalArgumentException(
+            String.format("Unsupported data type for %s(dataColumn, 
timeColumn): %s", name, dataType));
+    }
+  }
+}
diff --git 
a/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
 
b/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
index ce5d25c7558..31531c92139 100644
--- 
a/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
+++ 
b/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
@@ -58,6 +58,7 @@ import org.apache.calcite.tools.RelBuilder;
 import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.pinot.calcite.rel.rules.ImmutablePinotSortExchangeCopyRule;
+import 
org.apache.pinot.calcite.rel.rules.PinotAggregateTypeArgumentRewriteRule;
 import org.apache.pinot.calcite.rel.rules.PinotImplicitTableHintRule;
 import org.apache.pinot.calcite.rel.rules.PinotJoinToDynamicBroadcastRule;
 import org.apache.pinot.calcite.rel.rules.PinotRelDistributionTraitRule;
@@ -604,6 +605,8 @@ public class QueryEnvironment {
     for (RelOptRule relOptRule : basicRules) {
       hepProgramBuilder.addRuleInstance(relOptRule);
     }
+    // Required to plan FIRST_WITH_TIME / LAST_WITH_TIME without a data type, 
so the rule options do not filter it.
+    
hepProgramBuilder.addRuleInstance(PinotAggregateTypeArgumentRewriteRule.INSTANCE);
 
     // ----
     // Pushdown filters using a single HepInstruction.
diff --git 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
index 63eec96d1dc..98d17a494c4 100644
--- 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
+++ 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
@@ -1814,6 +1814,17 @@ public class QueryCompilationTest extends 
QueryEnvironmentTestBase {
     
assertTrue(exception.getMessage().contains(QueryOptionKey.INFER_PARTITION_HINT),
 exception.getMessage());
   }
 
+  @Test
+  public void testValueWithTimeRejectsUnsupportedDataArgument() {
+    // The two-argument form infers the data type from the data argument, 
which cannot be multi-value.
+    QueryException exception = expectThrows(QueryException.class, () -> 
_queryEnvironment.explainQuery(
+        "SELECT FIRST_WITH_TIME(mcol1, ts) FROM e", 
RANDOM_REQUEST_ID_GEN.nextLong()));
+    assertEquals(exception.getErrorCode(), QueryErrorCode.QUERY_PLANNING);
+    assertTrue(
+        exception.getMessage().contains("Unsupported data type for 
FIRSTWITHTIME(dataColumn, timeColumn): ARRAY"),
+        exception.getMessage());
+  }
+
   @DataProvider(name = "testQueryExceptionDataProvider")
   private Object[][] provideQueriesWithException() {
     return new Object[][]{
diff --git 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryPlannerRuleOptionsTest.java
 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryPlannerRuleOptionsTest.java
index be830d02240..7ed615549e2 100644
--- 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryPlannerRuleOptionsTest.java
+++ 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryPlannerRuleOptionsTest.java
@@ -21,6 +21,7 @@ package org.apache.pinot.query;
 import java.util.HashMap;
 import java.util.Map;
 import org.apache.calcite.sql.SqlNode;
+import 
org.apache.pinot.calcite.rel.rules.PinotAggregateTypeArgumentRewriteRule;
 import org.apache.pinot.common.utils.config.QueryOptionsUtils;
 import org.apache.pinot.spi.utils.CommonConstants;
 import org.apache.pinot.spi.utils.CommonConstants.Broker.PlannerRuleNames;
@@ -93,6 +94,15 @@ public class QueryPlannerRuleOptionsTest extends 
QueryEnvironmentTestBase {
     //@formatter:on
   }
 
+  @Test
+  public void testTypeArgumentRewriteIgnoresSkipPlannerRules() {
+    // The servers require the data type argument, so naming the rule in 
skipPlannerRules does not turn it off.
+    String query = "EXPLAIN PLAN FOR SELECT FIRST_WITH_TIME(col1, ts) FROM a";
+    String explain = explainQueryWithRuleDisabled(query, 
PinotAggregateTypeArgumentRewriteRule.INSTANCE.toString());
+    assertEquals(explain, _queryEnvironment.explainQuery(query, 
RANDOM_REQUEST_ID_GEN.nextLong()));
+    assertTrue(explain.contains("FIRSTWITHTIME($0, $1, _UTF-8'STRING')"), 
explain);
+  }
+
   @Test
   public void testDisableReduceFunctions() {
     // Tests that when skipPinotAggregateReduceFunctionsRule=true,
diff --git a/pinot-query-planner/src/test/resources/queries/AggregatePlans.json 
b/pinot-query-planner/src/test/resources/queries/AggregatePlans.json
index 364a54fda9d..f1dbfd4813e 100644
--- a/pinot-query-planner/src/test/resources/queries/AggregatePlans.json
+++ b/pinot-query-planner/src/test/resources/queries/AggregatePlans.json
@@ -158,6 +158,45 @@
           "\n"
         ]
       },
+      {
+        "description": "FIRST_WITH_TIME and LAST_WITH_TIME without a data type 
infer it from the data argument",
+        "sql": "EXPLAIN PLAN FOR SELECT FIRST_WITH_TIME(a.col1, a.ts), 
LAST_WITH_TIME(a.ts_timestamp, a.ts), FIRST_WITH_TIME(a.col5, a.ts), 
LAST_WITH_TIME(a.col1, a.ts) FROM a",
+        "output": [
+          "Execution Plan",
+          "\nPinotLogicalAggregate(group=[{}], agg#0=[FIRSTWITHTIME($0, $0, 
_UTF-8'STRING')], agg#1=[LASTWITHTIME($1, $1, _UTF-8'TIMESTAMP')], 
agg#2=[FIRSTWITHTIME($2, $2, _UTF-8'BOOLEAN')], agg#3=[LASTWITHTIME($3, $3, 
_UTF-8'STRING')], aggType=[FINAL])",
+          "\n  PinotLogicalExchange(distribution=[hash])",
+          "\n    PinotLogicalAggregate(group=[{}], agg#0=[FIRSTWITHTIME($0, 
$1, _UTF-8'STRING')], agg#1=[LASTWITHTIME($2, $1, _UTF-8'TIMESTAMP')], 
agg#2=[FIRSTWITHTIME($3, $1, _UTF-8'BOOLEAN')], agg#3=[LASTWITHTIME($0, $1, 
_UTF-8'STRING')], aggType=[LEAF])",
+          "\n      LogicalProject($f0=[$0], $f1=[$7], $f2=[$8], $f3=[$4], 
$f4=[_UTF-8'STRING'], $f5=[_UTF-8'TIMESTAMP'], $f6=[_UTF-8'BOOLEAN'])",
+          "\n        PinotLogicalTableScan(table=[[default, a]])",
+          "\n"
+        ]
+      },
+      {
+        "description": "Inferred data type argument next to another literal 
argument in a grouped aggregate",
+        "sql": "EXPLAIN PLAN FOR SELECT a.col2, FIRST_WITH_TIME(a.col3 + 1, 
a.ts), PERCENTILE(a.col3, 50) FROM a GROUP BY a.col2",
+        "output": [
+          "Execution Plan",
+          "\nPinotLogicalAggregate(group=[{0}], agg#0=[FIRSTWITHTIME($1, $1, 
_UTF-8'INT')], agg#1=[PERCENTILE($2, 50)], aggType=[FINAL])",
+          "\n  PinotLogicalExchange(distribution=[hash[0]])",
+          "\n    PinotLogicalAggregate(group=[{0}], agg#0=[FIRSTWITHTIME($1, 
$2, _UTF-8'INT')], agg#1=[PERCENTILE($3, 50)], aggType=[LEAF])",
+          "\n      LogicalProject($f0=[$1], $f1=[+($2, 1)], $f2=[$7], 
$f3=[$2], $f4=[50], $f5=[_UTF-8'INT'])",
+          "\n        PinotLogicalTableScan(table=[[default, a]])",
+          "\n"
+        ]
+      },
+      {
+        "description": "FIRST_WITH_TIME and LAST_WITH_TIME without a data type 
infer BIG_DECIMAL and BYTES",
+        "sql": "EXPLAIN PLAN FOR SELECT FIRST_WITH_TIME(a.col4, a.ts), 
LAST_WITH_TIME(hexToBytes(a.col1), a.ts) FROM a",
+        "output": [
+          "Execution Plan",
+          "\nPinotLogicalAggregate(group=[{}], agg#0=[FIRSTWITHTIME($0, $0, 
_UTF-8'BIG_DECIMAL')], agg#1=[LASTWITHTIME($1, $1, _UTF-8'BYTES')], 
aggType=[FINAL])",
+          "\n  PinotLogicalExchange(distribution=[hash])",
+          "\n    PinotLogicalAggregate(group=[{}], agg#0=[FIRSTWITHTIME($0, 
$1, _UTF-8'BIG_DECIMAL')], agg#1=[LASTWITHTIME($2, $1, _UTF-8'BYTES')], 
aggType=[LEAF])",
+          "\n      LogicalProject($f0=[$3], $f1=[$7], $f2=[HEXTOBYTES($0)], 
$f3=[_UTF-8'BIG_DECIMAL'], $f4=[_UTF-8'BYTES'])",
+          "\n        PinotLogicalTableScan(table=[[default, a]])",
+          "\n"
+        ]
+      },
       {
         "description": "MIN on STRING column rewritten to MINSTRING",
         "sql": "SET autoRewriteAggregationType=true; EXPLAIN PLAN FOR SELECT 
MIN(a.col1) FROM a",
diff --git 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTestBase.java
 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTestBase.java
index ea375c4069d..ddd6070414e 100644
--- 
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTestBase.java
+++ 
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/runtime/queries/QueryRunnerTestBase.java
@@ -73,6 +73,7 @@ import org.apache.pinot.spi.utils.CommonConstants.Broker;
 import 
org.apache.pinot.spi.utils.CommonConstants.Broker.Request.QueryOptionKey;
 import org.apache.pinot.spi.utils.CommonConstants.Query.Request.MetadataKeys;
 import org.apache.pinot.spi.utils.StringUtil;
+import org.apache.pinot.spi.utils.UuidUtils;
 import org.apache.pinot.sql.parsers.CalciteSqlParser;
 import org.apache.pinot.sql.parsers.SqlNodeAndOptions;
 import org.h2.jdbc.JdbcArray;
@@ -319,6 +320,8 @@ public abstract class QueryRunnerTestBase extends 
QueryTestSet {
         assertTrue(value instanceof String,
             "Got unexpected value type: " + value.getClass() + " for BYTES 
column, expected: String or byte[]");
         return value;
+      case UUID:
+        return UuidUtils.toUUID(value).toString();
       case INT_ARRAY:
         if (value instanceof List) {
           return ((List) value).stream().mapToInt(i -> (int) i).toArray();
@@ -427,6 +430,7 @@ public abstract class QueryRunnerTestBase extends 
QueryTestSet {
       case TIMESTAMP:
       case STRING:
       case BYTES:
+      case UUID:
         return actual.equals(expected);
       case INT_ARRAY:
         return Arrays.equals((int[]) actual, (int[]) expected);
@@ -523,6 +527,9 @@ public abstract class QueryRunnerTestBase extends 
QueryTestSet {
               case BYTES:
                 h2Statement.setBytes(h2Index++, Hex.decodeHex((String) value));
                 break;
+              case UUID:
+                h2Statement.setObject(h2Index++, UuidUtils.toUUID(value));
+                break;
               default:
                 h2Statement.setObject(h2Index++, value);
                 break;
@@ -566,6 +573,8 @@ public abstract class QueryRunnerTestBase extends 
QueryTestSet {
         return "BYTEA";
       case TIMESTAMP:
         return "TIMESTAMP";
+      case UUID:
+        return "UUID";
       default:
         throw new UnsupportedOperationException("Unsupported type conversion 
to h2 type: " + dataType);
     }
diff --git a/pinot-query-runtime/src/test/resources/queries/UDFAggregates.json 
b/pinot-query-runtime/src/test/resources/queries/UDFAggregates.json
index a99ba06694f..ca555e963d8 100644
--- a/pinot-query-runtime/src/test/resources/queries/UDFAggregates.json
+++ b/pinot-query-runtime/src/test/resources/queries/UDFAggregates.json
@@ -10,16 +10,18 @@
           {"name": "string_col", "type": "STRING"},
           {"name": "bool_col", "type": "BOOLEAN"},
           {"name": "decimal_col", "type": "BIG_DECIMAL"},
-          {"name": "timestamp_col", "type": "TIMESTAMP"}
+          {"name": "timestamp_col", "type": "TIMESTAMP"},
+          {"name": "bytes_col", "type": "BYTES"},
+          {"name": "uuid_col", "type": "UUID"}
         ],
         "inputs": [
-          [2, 2, 300, 300, "a", true, "1E-307", "2020-01-01 03:10:12"],
-          [2, 2, 400, 400, "a", true, "1E+307", "2020-01-01 03:39:12"],
-          [3, 3, 100, 100, "b", false, "1E-308", "2020-01-01 08:32:12"],
-          [100, 100, 1, 1, "b", false, "1E+308", "2020-02-06 03:32:12"],
-          [101, 101, 1.01, 1.01, "c", false, "1E-317", "2020-05-01 03:32:12"],
-          [150, 150, 1.5, 1.5, "c", false, "1E+317", "2020-02-03 03:32:12"],
-          [175, 175, 1.75, 1.75, "c", true, "1E+328", "2020-01-02 03:32:12"]
+          [2, 2, 300, 300, "a", true, "1E-307", "2020-01-01 03:10:12", 
"DEADBEEF", "00000000-0000-0000-0000-000000000001"],
+          [2, 2, 400, 400, "a", true, "1E+307", "2020-01-01 03:39:12", 
"FDEADBEE", "00000000-0000-0000-0000-000000000002"],
+          [3, 3, 100, 100, "b", false, "1E-308", "2020-01-01 08:32:12", 
"EFDEADBE", "00000000-0000-0000-0000-000000000003"],
+          [100, 100, 1, 1, "b", false, "1E+308", "2020-02-06 03:32:12", 
"BEEFDEAD", "00000000-0000-0000-0000-000000000004"],
+          [101, 101, 1.01, 1.01, "c", false, "1E-317", "2020-05-01 03:32:12", 
"ADBEEFDE", "00000000-0000-0000-0000-000000000005"],
+          [150, 150, 1.5, 1.5, "c", false, "1E+317", "2020-02-03 03:32:12", 
"DEADBE01", "00000000-0000-0000-0000-000000000006"],
+          [175, 175, 1.75, 1.75, "c", true, "1E+328", "2020-01-02 03:32:12", 
"DEADBE02", "00000000-0000-0000-0000-000000000007"]
         ]
       }
     },
@@ -48,6 +50,64 @@
         "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_aggregate='true') */ 
string_col, LASTWITHTIME(float_col, timestamp_col, 'FLOAT'), 
LASTWITHTIME(double_col, timestamp_col, 'DOUBLE'), last_with_time(long_col, 
CAST(long_col AS TIMESTAMP), 'LONG') FROM {tbl} GROUP BY string_col",
         "outputs": [["a", 400, 400, 2], ["b", 1, 1, 100], ["c", 1.01, 1.01, 
175]]
       },
+      {
+        "comment": "The data type argument of FIRST_WITH_TIME / LAST_WITH_TIME 
is inferred when omitted",
+        "sql": "SELECT FIRSTWITHTIME(int_col, timestamp_col), 
FIRSTWITHTIME(string_col, timestamp_col), first_with_time(bool_col, 
CAST(long_col AS TIMESTAMP)) FROM {tbl}",
+        "outputs": [[2, "a", true]]
+      },
+      {
+        "sql": "SELECT bool_col, FIRSTWITHTIME(float_col, timestamp_col), 
FIRSTWITHTIME(double_col, timestamp_col), first_with_time(long_col, 
CAST(long_col AS TIMESTAMP)) FROM {tbl} GROUP BY bool_col",
+        "outputs": [[false, 100, 100, 3], [true, 300, 300, 2]]
+      },
+      {
+        "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_aggregate='true') */ 
string_col, FIRSTWITHTIME(float_col, timestamp_col), FIRSTWITHTIME(double_col, 
timestamp_col), first_with_time(long_col, CAST(long_col AS TIMESTAMP)) FROM 
{tbl} GROUP BY string_col",
+        "outputs": [["a", 300, 300, 2], ["b", 100, 100, 3], ["c", 1.75, 1.75, 
101]]
+      },
+      {
+        "sql": "SELECT LASTWITHTIME(int_col, timestamp_col), 
LASTWITHTIME(string_col, timestamp_col), last_with_time(bool_col, CAST(long_col 
AS TIMESTAMP)) FROM {tbl}",
+        "outputs": [[101, "c", true]]
+      },
+      {
+        "sql": "SELECT bool_col, LASTWITHTIME(float_col, timestamp_col), 
LASTWITHTIME(double_col, timestamp_col), last_with_time(long_col, CAST(long_col 
AS TIMESTAMP)) FROM {tbl} GROUP BY bool_col",
+        "outputs": [[false, 1.01, 1.01, 150], [true, 1.75, 1.75, 175]]
+      },
+      {
+        "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_aggregate='true') */ 
string_col, LASTWITHTIME(float_col, timestamp_col), LASTWITHTIME(double_col, 
timestamp_col), last_with_time(long_col, CAST(long_col AS TIMESTAMP)) FROM 
{tbl} GROUP BY string_col",
+        "outputs": [["a", 400, 400, 2], ["b", 1, 1, 100], ["c", 1.01, 1.01, 
175]]
+      },
+      {
+        "sql": "SELECT FIRST_WITH_TIME(timestamp_col, timestamp_col), 
LAST_WITH_TIME(timestamp_col, timestamp_col), FIRST_WITH_TIME(timestamp_col, 
timestamp_col, 'TIMESTAMP') FROM {tbl}",
+        "outputs": [["2020-01-01 03:10:12.0", "2020-05-01 03:32:12.0", 
"2020-01-01 03:10:12.0"]]
+      },
+      {
+        "sql": "SELECT FIRST_WITH_TIME(uuid_col, timestamp_col), 
LAST_WITH_TIME(uuid_col, timestamp_col) FROM {tbl}",
+        "outputs": [["00000000-0000-0000-0000-000000000001", 
"00000000-0000-0000-0000-000000000005"]]
+      },
+      {
+        "sql": "SELECT string_col, FIRST_WITH_TIME(uuid_col, timestamp_col), 
LAST_WITH_TIME(uuid_col, timestamp_col) FROM {tbl} GROUP BY string_col",
+        "outputs": [["a", "00000000-0000-0000-0000-000000000001", 
"00000000-0000-0000-0000-000000000002"], ["b", 
"00000000-0000-0000-0000-000000000003", 
"00000000-0000-0000-0000-000000000004"], ["c", 
"00000000-0000-0000-0000-000000000007", "00000000-0000-0000-0000-000000000005"]]
+      },
+      {
+        "comment": "SMALLINT and TINYINT data arguments infer 'INT'",
+        "sql": "SELECT FIRST_WITH_TIME(CAST(int_col AS SMALLINT), 
timestamp_col), LAST_WITH_TIME(CAST(int_col AS SMALLINT), timestamp_col), 
LAST_WITH_TIME(CAST(int_col AS TINYINT), timestamp_col) FROM {tbl}",
+        "outputs": [[2, 101, 101]]
+      },
+      {
+        "sql": "SELECT string_col, FIRST_WITH_TIME(CAST(int_col AS SMALLINT), 
timestamp_col), LAST_WITH_TIME(CAST(int_col AS SMALLINT), timestamp_col) FROM 
{tbl} GROUP BY string_col",
+        "outputs": [["a", 2, 2], ["b", 3, 100], ["c", 175, 101]]
+      },
+      {
+        "sql": "SELECT FIRST_WITH_TIME(decimal_col, timestamp_col), 
LAST_WITH_TIME(decimal_col, timestamp_col), FIRST_WITH_TIME(bytes_col, 
timestamp_col), LAST_WITH_TIME(bytes_col, timestamp_col, 'BYTES') FROM {tbl}",
+        "outputs": [["1E-307", "1E-317", "deadbeef", "adbeefde"]]
+      },
+      {
+        "sql": "SELECT string_col, FIRST_WITH_TIME(decimal_col, 
timestamp_col), LAST_WITH_TIME(decimal_col, timestamp_col), 
FIRST_WITH_TIME(bytes_col, timestamp_col), LAST_WITH_TIME(bytes_col, 
timestamp_col) FROM {tbl} GROUP BY string_col",
+        "outputs": [["a", "1E-307", "1E+307", "deadbeef", "fdeadbee"], ["b", 
"1E-308", "1E+308", "efdeadbe", "beefdead"], ["c", "1E+328", "1E-317", 
"deadbe02", "adbeefde"]]
+      },
+      {
+        "sql": "SELECT /*+ aggOptions(is_skip_leaf_stage_aggregate='true') */ 
string_col, FIRST_WITH_TIME(decimal_col, timestamp_col), 
LAST_WITH_TIME(decimal_col, timestamp_col), FIRST_WITH_TIME(bytes_col, 
timestamp_col), LAST_WITH_TIME(bytes_col, timestamp_col) FROM {tbl} GROUP BY 
string_col",
+        "outputs": [["a", "1E-307", "1E+307", "deadbeef", "fdeadbee"], ["b", 
"1E-308", "1E+308", "efdeadbe", "beefdead"], ["c", "1E+328", "1E-317", 
"deadbe02", "adbeefde"]]
+      },
       {
         "sql": "SELECT SUMPRECISION(decimal_col) FROM {tbl}",
         "outputs": 
[["10000000000100000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
 [...]
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/BigDecimalLongPair.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/BigDecimalLongPair.java
new file mode 100644
index 00000000000..95032c4fe86
--- /dev/null
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/BigDecimalLongPair.java
@@ -0,0 +1,52 @@
+/**
+ * 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.segment.local.customobject;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import org.apache.pinot.spi.utils.BigDecimalUtils;
+
+
+/// A BIG_DECIMAL value with its time, for FIRST_WITH_TIME / LAST_WITH_TIME.
+public class BigDecimalLongPair extends ValueLongPair<BigDecimal> {
+
+  public BigDecimalLongPair(BigDecimal value, long time) {
+    super(value, time);
+  }
+
+  public static BigDecimalLongPair fromBytes(byte[] bytes) {
+    return fromByteBuffer(ByteBuffer.wrap(bytes));
+  }
+
+  public static BigDecimalLongPair fromByteBuffer(ByteBuffer byteBuffer) {
+    byte[] valueBytes = new byte[byteBuffer.getInt()];
+    byteBuffer.get(valueBytes);
+    return new BigDecimalLongPair(BigDecimalUtils.deserialize(valueBytes), 
byteBuffer.getLong());
+  }
+
+  @Override
+  public byte[] toBytes() {
+    byte[] valueBytes = BigDecimalUtils.serialize(_value);
+    ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.BYTES + 
valueBytes.length + Long.BYTES);
+    byteBuffer.putInt(valueBytes.length);
+    byteBuffer.put(valueBytes);
+    byteBuffer.putLong(_time);
+    return byteBuffer.array();
+  }
+}
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/BytesLongPair.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/BytesLongPair.java
new file mode 100644
index 00000000000..517eaffdc54
--- /dev/null
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/customobject/BytesLongPair.java
@@ -0,0 +1,51 @@
+/**
+ * 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.segment.local.customobject;
+
+import java.nio.ByteBuffer;
+import org.apache.pinot.spi.utils.ByteArray;
+
+
+/// A BYTES value with its time, for FIRST_WITH_TIME / LAST_WITH_TIME. UUID 
values use the same pair.
+public class BytesLongPair extends ValueLongPair<ByteArray> {
+
+  public BytesLongPair(ByteArray value, long time) {
+    super(value, time);
+  }
+
+  public static BytesLongPair fromBytes(byte[] bytes) {
+    return fromByteBuffer(ByteBuffer.wrap(bytes));
+  }
+
+  public static BytesLongPair fromByteBuffer(ByteBuffer byteBuffer) {
+    byte[] valueBytes = new byte[byteBuffer.getInt()];
+    byteBuffer.get(valueBytes);
+    return new BytesLongPair(new ByteArray(valueBytes), byteBuffer.getLong());
+  }
+
+  @Override
+  public byte[] toBytes() {
+    byte[] valueBytes = _value.getBytes();
+    ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.BYTES + 
valueBytes.length + Long.BYTES);
+    byteBuffer.putInt(valueBytes.length);
+    byteBuffer.put(valueBytes);
+    byteBuffer.putLong(_time);
+    return byteBuffer.array();
+  }
+}
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 06d04efbb00..7534ec713f6 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
@@ -65,10 +65,12 @@ public enum AggregationFunctionType {
   AVG("avg", SqlTypeName.OTHER, SqlTypeName.DOUBLE),
   MODE("mode", SqlTypeName.OTHER, SqlTypeName.DOUBLE),
   ANYVALUE("anyValue", ReturnTypes.ARG0, OperandTypes.ANY, SqlTypeName.OTHER),
-  FIRSTWITHTIME("firstWithTime", ReturnTypes.ARG0,
-      OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.ANY, 
SqlTypeFamily.CHARACTER), SqlTypeName.OTHER),
-  LASTWITHTIME("lastWithTime", ReturnTypes.ARG0,
-      OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.ANY, 
SqlTypeFamily.CHARACTER), SqlTypeName.OTHER),
+  FIRSTWITHTIME("firstWithTime", ReturnTypes.ARG0, OperandTypes.or(
+      OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.ANY),
+      OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.ANY, 
SqlTypeFamily.CHARACTER)), SqlTypeName.OTHER),
+  LASTWITHTIME("lastWithTime", ReturnTypes.ARG0, OperandTypes.or(
+      OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.ANY),
+      OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.ANY, 
SqlTypeFamily.CHARACTER)), SqlTypeName.OTHER),
   MINMAXRANGE("minMaxRange", ReturnTypes.DOUBLE, OperandTypes.ANY, 
SqlTypeName.OTHER, SqlTypeName.DOUBLE),
 
   /// for all distinct count family functions:


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

Reply via email to