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

Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new af6dcff9051 [opt](paimon) Reduce JNI read object allocations (#66244)
af6dcff9051 is described below

commit af6dcff905114d7e05a6101496f5a629d8831b64
Author: Gabriel <[email protected]>
AuthorDate: Thu Jul 30 18:05:53 2026 +0800

    [opt](paimon) Reduce JNI read object allocations (#66244)
    
    Problem Summary:
    
    The Paimon JNI read path created a new PaimonColumnValue for every array 
element, map key/value, and struct field on every row. Timestamp conversions 
also resolved the session time zone per value and built redundant intermediate 
temporal objects. On wide nested data or large scan batches, these short-lived 
objects increase allocation rate and GC pressure.
    
    For example, consider two consecutive ARRAY<INT> rows, [10, 20] and [30, 
40]. Previously, reading the second row allocated two new wrappers even though 
the first row's wrappers had already been consumed. This PR keeps two 
position-specific wrappers and resets their record, index, Doris type, Paimon 
type, and time zone before reading the second row. This is safe because 
VectorColumn consumes the unpacked list synchronously before the scanner 
advances to another value.
    
    The same invariant is applied separately to array elements, map keys, map 
values, and struct fields. Caches are lazy, grow only when needed, and only the 
current collection size or projected struct indexes are emitted, so shrinking 
collections cannot expose stale entries. Struct wrappers are cached by the 
original field index, preserving sparse/non-contiguous projection behavior 
fixed by #66223.
    
    For temporal values, the session time-zone string is now parsed once into a 
ZoneId. A Paimon TIMESTAMP_LTZ epoch instant is converted directly into that 
zone instead of constructing UTC and target-zone intermediates. TIMESTAMPTZ 
uses Paimon's equivalent local representation directly instead of converting 
through Instant and UTC.
    
    What changed?
    Lazily reuse nested PaimonColumnValue wrappers across rows for arrays, 
maps, structs, and recursively nested values.
    Reset all row-dependent state before a cached wrapper is reused.
    Cache the parsed session ZoneId instead of resolving it for every value.
    Remove redundant temporal conversion objects while preserving 
negative-epoch, nanosecond, and DST behavior.
    Add unit coverage for wrapper identity reuse, growing/shrinking arrays, 
maps, sparse struct projections, nested arrays, DST, negative epoch, and 
nanosecond precision.
    Local allocation and latency microbenchmark
    Environment: Linux x86_64, Oracle JDK 17.0.16, Paimon 1.3.1. The benchmark 
ran latest master and this commit in separate worktrees on the same machine. 
Each case used 100,000 warmup iterations and 5 measured trials; the table 
reports medians. Collection cases ran 300,000 operations per trial, timestamp 
cases ran 1,000,000. Allocation was measured with ThreadMXBean; GC count/time 
was measured with GarbageCollectorMXBean. A full GC was requested before each 
measured trial and excluded f [...]
    
    The collection output lists were preallocated and cleared between 
operations to isolate allocation inside the Paimon value conversion path. 
Therefore these numbers are a focused Java microbenchmark, not an end-to-end 
query throughput claim.
    
    Benchmark data shape and values:
    
    ARRAY<INT>[16]: one GenericRow containing one GenericArray with [0, 1, ..., 
15].
    MAP<INT,BIGINT>[16]: one GenericRow containing one insertion-ordered 
GenericMap with {0: 0, 1: 1, ..., 15: 15}.
    STRUCT<INT,STRING,BIGINT>: one binary row containing (10, "x", 100); all 
three fields are projected.
    TIMESTAMP_LTZ(9): one fixed instant, 2024-03-10T10:30:00.123456789Z, read 
in session zone America/Los_Angeles. This date exercises the US DST transition 
day.
    TIMESTAMPTZ(9): one fixed pre-epoch value, 1969-12-31T23:59:59.999999999Z, 
read in UTC.
    Every benchmark operation rereads the same Paimon row/value. This 
intentionally removes file I/O, row construction, and table scan variability, 
and isolates the allocation/latency of PaimonColumnValue.unpack*() or timestamp 
conversion. The core collection loop is equivalent to:
    
    values.clear();                 // preallocated outside the measured loop
    columnValue.unpackArray(values);
    consume(values.get(0), values.get(15));
    On master, each unpackArray call above creates 16 wrapper objects. With 
this PR, the warmup creates the 16 position-specific wrappers once and measured 
iterations only reset them. Map and struct use the same loop shape with 
separate key/value or projected-field output lists.
    
    Case    master alloc (B/op)     PR alloc (B/op) Allocation reduction    
master time (ns/op)     PR time (ns/op) Time reduction  Median GC 
collections/trial
    ARRAY<INT>[16]  512.03  0.03    99.99%  368.38  141.61  61.6%   2 → 0
    MAP<INT,BIGINT>[16]     1232.03 208.03  83.1%   668.39  508.57  23.9%   2 → 
1
    STRUCT<INT,STRING,BIGINT>       128.03  32.03   75.0%   63.74   63.73   ~0% 
    0 → 0
    TIMESTAMP_LTZ(9)        384.01  64.01   83.3%   156.25  57.88   63.0%   2 → 
1
    TIMESTAMPTZ(9)  200.01  48.01   76.0%   44.45   21.65   51.3%   1 → 0
    The remaining map allocation comes primarily from Paimon's 
GenericMap.keyArray() / valueArray() materialization, which is unchanged by 
this PR. Struct latency is effectively unchanged while allocation drops by 75%.
---
 .../org/apache/doris/paimon/PaimonColumnValue.java | 131 ++++++++++--
 .../apache/doris/paimon/PaimonColumnValueTest.java | 236 ++++++++++++++++++++-
 .../apache/doris/paimon/PaimonJniScannerTest.java  |  10 +
 3 files changed, 354 insertions(+), 23 deletions(-)

diff --git 
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonColumnValue.java
 
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonColumnValue.java
index f60bb996703..339feb97c9b 100644
--- 
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonColumnValue.java
+++ 
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonColumnValue.java
@@ -38,20 +38,47 @@ import java.math.BigInteger;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class PaimonColumnValue implements ColumnValue {
     private static final Logger LOG = 
LoggerFactory.getLogger(PaimonColumnValue.class);
+    private static final Map<String, String> DORIS_TIME_ZONE_ALIASES;
+
+    static {
+        Map<String, String> aliases = new HashMap<>(ZoneId.SHORT_IDS);
+        // The scanner cannot depend on FE's TimeUtils, so keep its accepted 
aliases and CST
+        // interpretation identical at this JNI boundary.
+        aliases.put("CST", "Asia/Shanghai");
+        aliases.put("PRC", "Asia/Shanghai");
+        aliases.put("UTC", "UTC");
+        aliases.put("GMT", "UTC");
+        DORIS_TIME_ZONE_ALIASES = Collections.unmodifiableMap(aliases);
+    }
+
     private int idx;
     private DataGetters record;
     private ColumnType dorisType;
     private DataType dataType;
-    private String timeZone;
+    private ZoneId timeZone;
+    // Keep these caches lazy so scalar columns do not pay for complex-type 
reuse bookkeeping.
+    private List<PaimonColumnValue> arrayValues;
+    private List<PaimonColumnValue> mapKeys;
+    private List<PaimonColumnValue> mapValues;
+    private List<PaimonColumnValue> structValues;
 
     public PaimonColumnValue() {
     }
 
     public PaimonColumnValue(DataGetters record, int idx, ColumnType 
columnType, DataType dataType, String timeZone) {
+        this(record, idx, columnType, dataType, resolveTimeZone(timeZone));
+    }
+
+    private PaimonColumnValue(
+            DataGetters record, int idx, ColumnType columnType, DataType 
dataType, ZoneId timeZone) {
         this.idx = idx;
         this.record = record;
         this.dorisType = columnType;
@@ -70,7 +97,7 @@ public class PaimonColumnValue implements ColumnValue {
     }
 
     public void setTimeZone(String timeZone) {
-        this.timeZone = timeZone;
+        this.timeZone = resolveTimeZone(timeZone);
     }
 
     @Override
@@ -142,8 +169,8 @@ public class PaimonColumnValue implements ColumnValue {
     public LocalDateTime getDateTime() {
         Timestamp ts = record.getTimestamp(idx, dorisType.getPrecision());
         if (dataType instanceof LocalZonedTimestampType) {
-            return ts.toLocalDateTime().atZone(ZoneId.of("UTC"))
-                    
.withZoneSameInstant(ZoneId.of(timeZone)).toLocalDateTime();
+            // Paimon stores TIMESTAMP_LTZ as an epoch instant, so convert it 
directly in the cached session zone.
+            return LocalDateTime.ofInstant(ts.toInstant(), timeZone);
         } else {
             return ts.toLocalDateTime();
         }
@@ -152,15 +179,18 @@ public class PaimonColumnValue implements ColumnValue {
     @Override
     public LocalDateTime getTimeStampTz() {
         Timestamp ts = record.getTimestamp(idx, dorisType.getPrecision());
-        LocalDateTime v = ts.toInstant()
-                .atZone(ZoneId.of("UTC"))
-                .toLocalDateTime();
-        return v;
+        // Timestamp's local representation is identical to converting its 
epoch instant in UTC.
+        return ts.toLocalDateTime();
     }
 
     @Override
     public boolean isNull() {
-        return record.isNullAt(idx);
+        boolean isNull = record.isNullAt(idx);
+        if (isNull) {
+            // A null complex value has no live descendants; release wrappers 
retained by its prior row.
+            clearChildCaches();
+        }
+        return isNull;
     }
 
     @Override
@@ -171,28 +201,41 @@ public class PaimonColumnValue implements ColumnValue {
     @Override
     public void unpackArray(List<ColumnValue> values) {
         InternalArray recordArray = record.getArray(idx);
+        if (arrayValues == null) {
+            arrayValues = new ArrayList<>();
+        }
+        ColumnType elementDorisType = dorisType.getChildTypes().get(0);
+        DataType elementPaimonType = ((ArrayType) dataType).getElementType();
         for (int i = 0; i < recordArray.size(); i++) {
-            PaimonColumnValue arrayColumnValue = new 
PaimonColumnValue((DataGetters) recordArray, i,
-                    dorisType.getChildTypes().get(0), ((ArrayType) 
dataType).getElementType(), timeZone);
-            values.add(arrayColumnValue);
+            values.add(reuseColumnValue(arrayValues, i, (DataGetters) 
recordArray, i,
+                    elementDorisType, elementPaimonType));
         }
+        trimCache(arrayValues, recordArray.size());
     }
 
     @Override
     public void unpackMap(List<ColumnValue> keys, List<ColumnValue> values) {
         InternalMap map = record.getMap(idx);
+        if (mapKeys == null) {
+            mapKeys = new ArrayList<>();
+            mapValues = new ArrayList<>();
+        }
         InternalArray key = map.keyArray();
+        ColumnType keyDorisType = dorisType.getChildTypes().get(0);
+        DataType keyPaimonType = ((MapType) dataType).getKeyType();
         for (int i = 0; i < key.size(); i++) {
-            PaimonColumnValue keyColumnValue = new 
PaimonColumnValue((DataGetters) key, i,
-                    dorisType.getChildTypes().get(0), ((MapType) 
dataType).getKeyType(), timeZone);
-            keys.add(keyColumnValue);
+            keys.add(reuseColumnValue(mapKeys, i, (DataGetters) key, i,
+                    keyDorisType, keyPaimonType));
         }
+        trimCache(mapKeys, key.size());
         InternalArray value = map.valueArray();
+        ColumnType valueDorisType = dorisType.getChildTypes().get(1);
+        DataType valuePaimonType = ((MapType) dataType).getValueType();
         for (int i = 0; i < value.size(); i++) {
-            PaimonColumnValue valueColumnValue = new 
PaimonColumnValue((DataGetters) value, i,
-                    dorisType.getChildTypes().get(1), ((MapType) 
dataType).getValueType(), timeZone);
-            values.add(valueColumnValue);
+            values.add(reuseColumnValue(mapValues, i, (DataGetters) value, i,
+                    valueDorisType, valuePaimonType));
         }
+        trimCache(mapValues, value.size());
     }
 
     @Override
@@ -200,9 +243,57 @@ public class PaimonColumnValue implements ColumnValue {
         RowType rowType = (RowType) dataType;
         // Projection entries are original child indexes, so the binary row 
must keep the full RowType arity.
         InternalRow row = record.getRow(idx, rowType.getFieldCount());
+        if (structValues == null) {
+            structValues = new ArrayList<>();
+        }
         for (int i : structFieldIndex) {
-            values.add(new PaimonColumnValue(row, i, 
dorisType.getChildTypes().get(i),
-                    rowType.getFields().get(i).type(), timeZone));
+            values.add(reuseColumnValue(structValues, i, row, i, 
dorisType.getChildTypes().get(i),
+                    rowType.getFields().get(i).type()));
+        }
+    }
+
+    private PaimonColumnValue reuseColumnValue(
+            List<PaimonColumnValue> cache, int cacheIndex, DataGetters 
childRecord, int childIndex,
+            ColumnType childDorisType, DataType childPaimonType) {
+        while (cache.size() <= cacheIndex) {
+            cache.add(null);
+        }
+        PaimonColumnValue value = cache.get(cacheIndex);
+        if (value == null) {
+            value = new PaimonColumnValue(
+                    childRecord, childIndex, childDorisType, childPaimonType, 
timeZone);
+            cache.set(cacheIndex, value);
+        } else {
+            // VectorColumn consumes unpacked values synchronously, before 
this parent advances to another value.
+            value.reset(childRecord, childIndex, childDorisType, 
childPaimonType, timeZone);
         }
+        return value;
+    }
+
+    private void reset(
+            DataGetters record, int idx, ColumnType dorisType, DataType 
dataType, ZoneId timeZone) {
+        this.record = record;
+        this.idx = idx;
+        this.dorisType = dorisType;
+        this.dataType = dataType;
+        this.timeZone = timeZone;
+    }
+
+    private static ZoneId resolveTimeZone(String timeZone) {
+        return ZoneId.of(timeZone, DORIS_TIME_ZONE_ALIASES);
+    }
+
+    private static void trimCache(List<PaimonColumnValue> cache, int liveSize) 
{
+        if (cache.size() > liveSize) {
+            // Retain only wrappers addressable by the current container, not 
its historical maximum.
+            cache.subList(liveSize, cache.size()).clear();
+        }
+    }
+
+    private void clearChildCaches() {
+        arrayValues = null;
+        mapKeys = null;
+        mapValues = null;
+        structValues = null;
     }
 }
diff --git 
a/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonColumnValueTest.java
 
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonColumnValueTest.java
index 639a2e87f30..20062ff2f6e 100644
--- 
a/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonColumnValueTest.java
+++ 
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonColumnValueTest.java
@@ -21,21 +21,33 @@ import org.apache.doris.common.jni.vec.ColumnType;
 import org.apache.doris.common.jni.vec.ColumnValue;
 
 import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericArray;
+import org.apache.paimon.data.GenericMap;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.Timestamp;
 import org.apache.paimon.data.serializer.InternalRowSerializer;
+import org.apache.paimon.types.ArrayType;
 import org.apache.paimon.types.BigIntType;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.IntType;
+import org.apache.paimon.types.LocalZonedTimestampType;
+import org.apache.paimon.types.MapType;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.TimestampType;
 import org.apache.paimon.types.VarCharType;
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.lang.reflect.Field;
+import java.time.Instant;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 public class PaimonColumnValueTest {
     private final RowType paimonStructType = RowType.of(
@@ -80,10 +92,228 @@ public class PaimonColumnValueTest {
         Assert.assertEquals(100L, values.get(2).getLong());
     }
 
+    @Test
+    public void testReuseArrayElementsAcrossRows() {
+        ArrayType paimonArrayType = new ArrayType(new IntType());
+        ColumnType dorisArrayType = ColumnType.parseType("a", "array<int>");
+        PaimonColumnValue arrayValue = new PaimonColumnValue(
+                GenericRow.of(new GenericArray(new int[] {1, 2})), 0,
+                dorisArrayType, paimonArrayType, "UTC");
+
+        List<ColumnValue> firstValues = new ArrayList<>();
+        arrayValue.unpackArray(firstValues);
+        arrayValue.setOffsetRow(GenericRow.of(new GenericArray(new int[] {3, 
4, 5})));
+        List<ColumnValue> secondValues = new ArrayList<>();
+        arrayValue.unpackArray(secondValues);
+
+        Assert.assertSame(firstValues.get(0), secondValues.get(0));
+        Assert.assertSame(firstValues.get(1), secondValues.get(1));
+        Assert.assertEquals(Arrays.asList(3, 4, 5), getInts(secondValues));
+
+        arrayValue.setOffsetRow(GenericRow.of(new GenericArray(new int[] 
{6})));
+        List<ColumnValue> thirdValues = new ArrayList<>();
+        arrayValue.unpackArray(thirdValues);
+        Assert.assertSame(firstValues.get(0), thirdValues.get(0));
+        Assert.assertEquals(Collections.singletonList(6), 
getInts(thirdValues));
+    }
+
+    @Test
+    public void testReuseMapEntriesAcrossRows() {
+        MapType paimonMapType = new MapType(new IntType(), new BigIntType());
+        ColumnType dorisMapType = ColumnType.parseType("m", "map<int,bigint>");
+        PaimonColumnValue mapValue = new PaimonColumnValue(
+                GenericRow.of(new GenericMap(linkedMap(1, 10L, 2, 20L))), 0,
+                dorisMapType, paimonMapType, "UTC");
+
+        List<ColumnValue> firstKeys = new ArrayList<>();
+        List<ColumnValue> firstValues = new ArrayList<>();
+        mapValue.unpackMap(firstKeys, firstValues);
+        mapValue.setOffsetRow(GenericRow.of(new GenericMap(linkedMap(3, 30L, 
4, 40L))));
+        List<ColumnValue> secondKeys = new ArrayList<>();
+        List<ColumnValue> secondValues = new ArrayList<>();
+        mapValue.unpackMap(secondKeys, secondValues);
+
+        Assert.assertSame(firstKeys.get(0), secondKeys.get(0));
+        Assert.assertSame(firstKeys.get(1), secondKeys.get(1));
+        Assert.assertSame(firstValues.get(0), secondValues.get(0));
+        Assert.assertSame(firstValues.get(1), secondValues.get(1));
+        Assert.assertEquals(Arrays.asList(3, 4), getInts(secondKeys));
+        Assert.assertEquals(Arrays.asList(30L, 40L), getLongs(secondValues));
+    }
+
+    @Test
+    public void testReuseProjectedStructFieldsAcrossRows() {
+        PaimonColumnValue structValue = createStructValue();
+        List<Integer> projectedFields = Arrays.asList(0, 2);
+        List<ColumnValue> firstValues = new ArrayList<>();
+        structValue.unpackStruct(projectedFields, firstValues);
+
+        structValue.setOffsetRow(createStructRow(20, "y", 200L));
+        List<ColumnValue> secondValues = new ArrayList<>();
+        structValue.unpackStruct(projectedFields, secondValues);
+
+        Assert.assertSame(firstValues.get(0), secondValues.get(0));
+        Assert.assertSame(firstValues.get(1), secondValues.get(1));
+        Assert.assertEquals(20, secondValues.get(0).getInt());
+        Assert.assertEquals(200L, secondValues.get(1).getLong());
+    }
+
+    @Test
+    public void testReuseNestedArrayElementsAcrossRows() {
+        ArrayType paimonNestedArrayType = new ArrayType(new ArrayType(new 
IntType()));
+        ColumnType dorisNestedArrayType = ColumnType.parseType("a", 
"array<array<int>>");
+        PaimonColumnValue arrayValue = new PaimonColumnValue(
+                GenericRow.of(new GenericArray(new Object[] {new 
GenericArray(new int[] {1, 2})})), 0,
+                dorisNestedArrayType, paimonNestedArrayType, "UTC");
+
+        List<ColumnValue> firstOuterValues = new ArrayList<>();
+        arrayValue.unpackArray(firstOuterValues);
+        List<ColumnValue> firstInnerValues = new ArrayList<>();
+        firstOuterValues.get(0).unpackArray(firstInnerValues);
+
+        arrayValue.setOffsetRow(
+                GenericRow.of(new GenericArray(new Object[] {new 
GenericArray(new int[] {3, 4})})));
+        List<ColumnValue> secondOuterValues = new ArrayList<>();
+        arrayValue.unpackArray(secondOuterValues);
+        List<ColumnValue> secondInnerValues = new ArrayList<>();
+        secondOuterValues.get(0).unpackArray(secondInnerValues);
+
+        Assert.assertSame(firstOuterValues.get(0), secondOuterValues.get(0));
+        Assert.assertSame(firstInnerValues.get(0), secondInnerValues.get(0));
+        Assert.assertSame(firstInnerValues.get(1), secondInnerValues.get(1));
+        Assert.assertEquals(Arrays.asList(3, 4), getInts(secondInnerValues));
+    }
+
+    @Test
+    public void testNestedArrayCacheRetainsOnlyCurrentShape() throws Exception 
{
+        int outerSize = 4;
+        int innerSize = 32;
+        ArrayType paimonNestedArrayType = new ArrayType(new ArrayType(new 
IntType()));
+        ColumnType dorisNestedArrayType = ColumnType.parseType("a", 
"array<array<int>>");
+        PaimonColumnValue arrayValue = new PaimonColumnValue(
+                nestedArrayRow(outerSize, innerSize, 0, -1), 0,
+                dorisNestedArrayType, paimonNestedArrayType, "UTC");
+
+        consumeNestedArray(arrayValue);
+        Assert.assertEquals(outerSize + innerSize, 
retainedCachedValues(arrayValue));
+
+        // Moving the large child to position 1 makes position 0 a smaller, 
non-null array.
+        arrayValue.setOffsetRow(nestedArrayRow(outerSize, innerSize, 1, -1));
+        consumeNestedArray(arrayValue);
+        Assert.assertEquals(outerSize + innerSize, 
retainedCachedValues(arrayValue));
+
+        // Moving it again makes position 1 null, which must release that 
position's descendants.
+        arrayValue.setOffsetRow(nestedArrayRow(outerSize, innerSize, 2, 1));
+        consumeNestedArray(arrayValue);
+        Assert.assertEquals(outerSize + innerSize, 
retainedCachedValues(arrayValue));
+    }
+
+    @Test
+    public void testTimestampConversionsPreserveBoundaryValues() {
+        Timestamp timestamp = Timestamp.fromEpochMillis(-1, 999_999);
+        ColumnType dorisTimestampType = ColumnType.parseType("t", 
"datetimev2(9)");
+
+        PaimonColumnValue timestampValue = new PaimonColumnValue(
+                GenericRow.of(timestamp), 0, dorisTimestampType, new 
TimestampType(9), "UTC");
+        Assert.assertEquals(
+                LocalDateTime.of(1969, 12, 31, 23, 59, 59, 999_999_999),
+                timestampValue.getDateTime());
+        Assert.assertEquals(
+                LocalDateTime.of(1969, 12, 31, 23, 59, 59, 999_999_999),
+                timestampValue.getTimeStampTz());
+
+        PaimonColumnValue localZonedValue = new PaimonColumnValue(
+                
GenericRow.of(Timestamp.fromInstant(Instant.parse("2024-03-10T10:30:00.123456789Z"))),
 0,
+                dorisTimestampType, new LocalZonedTimestampType(9), 
"America/Los_Angeles");
+        Assert.assertEquals(
+                LocalDateTime.of(2024, 3, 10, 3, 30, 0, 123_456_789),
+                localZonedValue.getDateTime());
+
+        localZonedValue.setTimeZone("Asia/Shanghai");
+        Assert.assertEquals(
+                LocalDateTime.of(2024, 3, 10, 18, 30, 0, 123_456_789),
+                localZonedValue.getDateTime());
+
+        localZonedValue.setTimeZone("CST");
+        Assert.assertEquals(
+                LocalDateTime.of(2024, 3, 10, 18, 30, 0, 123_456_789),
+                localZonedValue.getDateTime());
+    }
+
+    private InternalRow nestedArrayRow(int outerSize, int innerSize, int 
populatedIndex, int nullIndex) {
+        Object[] outerValues = new Object[outerSize];
+        for (int i = 0; i < outerSize; i++) {
+            if (i == nullIndex) {
+                outerValues[i] = null;
+            } else if (i == populatedIndex) {
+                outerValues[i] = new GenericArray(new int[innerSize]);
+            } else {
+                outerValues[i] = new GenericArray(new int[0]);
+            }
+        }
+        return GenericRow.of(new GenericArray(outerValues));
+    }
+
+    private void consumeNestedArray(PaimonColumnValue arrayValue) {
+        List<ColumnValue> outerValues = new ArrayList<>();
+        arrayValue.unpackArray(outerValues);
+        for (ColumnValue outerValue : outerValues) {
+            if (!outerValue.isNull()) {
+                outerValue.unpackArray(new ArrayList<>());
+            }
+        }
+    }
+
+    private int retainedCachedValues(PaimonColumnValue value) throws Exception 
{
+        int retained = 0;
+        for (String fieldName : Arrays.asList("arrayValues", "mapKeys", 
"mapValues", "structValues")) {
+            Field field = PaimonColumnValue.class.getDeclaredField(fieldName);
+            field.setAccessible(true);
+            List<?> children = (List<?>) field.get(value);
+            if (children == null) {
+                continue;
+            }
+            for (Object child : children) {
+                if (child != null) {
+                    retained++;
+                    retained += retainedCachedValues((PaimonColumnValue) 
child);
+                }
+            }
+        }
+        return retained;
+    }
+
     private PaimonColumnValue createStructValue() {
-        GenericRow nestedRow = GenericRow.of(10, BinaryString.fromString("x"), 
100L);
+        return new PaimonColumnValue(createStructRow(10, "x", 100L), 0,
+                dorisStructType, paimonStructType, "UTC");
+    }
+
+    private InternalRow createStructRow(int intValue, String stringValue, long 
longValue) {
+        GenericRow nestedRow = GenericRow.of(intValue, 
BinaryString.fromString(stringValue), longValue);
         RowType outerType = RowType.of(new DataType[] {paimonStructType}, new 
String[] {"s"});
-        InternalRow outerRow = new 
InternalRowSerializer(outerType).toBinaryRow(GenericRow.of(nestedRow));
-        return new PaimonColumnValue(outerRow, 0, dorisStructType, 
paimonStructType, "UTC");
+        return new 
InternalRowSerializer(outerType).toBinaryRow(GenericRow.of(nestedRow));
+    }
+
+    private List<Integer> getInts(List<ColumnValue> values) {
+        List<Integer> result = new ArrayList<>();
+        for (ColumnValue value : values) {
+            result.add(value.getInt());
+        }
+        return result;
+    }
+
+    private List<Long> getLongs(List<ColumnValue> values) {
+        List<Long> result = new ArrayList<>();
+        for (ColumnValue value : values) {
+            result.add(value.getLong());
+        }
+        return result;
+    }
+
+    private Map<Integer, Long> linkedMap(int key1, long value1, int key2, long 
value2) {
+        Map<Integer, Long> result = new LinkedHashMap<>();
+        result.put(key1, value1);
+        result.put(key2, value2);
+        return result;
     }
 }
diff --git 
a/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
 
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
index d43f251f58d..fd7d6958dd3 100644
--- 
a/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
+++ 
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
@@ -52,6 +52,16 @@ public class PaimonJniScannerTest {
         new PaimonJniScanner(128, createBaseParams());
     }
 
+    @Test
+    public void 
testConstructorAcceptsDorisShortTimeZoneForNonTemporalProjection() {
+        Map<String, String> params = createBaseParams();
+        params.put("required_fields", "id");
+        params.put("columns_types", "int");
+        params.put("time_zone", "EST");
+
+        new PaimonJniScanner(128, params);
+    }
+
     @Test
     public void testIOManagerOptionHelpers() throws Exception {
         Map<String, String> params = createBaseParams();


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

Reply via email to