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

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


The following commit(s) were added to refs/heads/master by this push:
     new b1e4862219 [codegen] Support normalized key for non-compact timestamp 
(#8759)
b1e4862219 is described below

commit b1e48622194fa34d9353d461a8a31948e531cd9b
Author: Vova Kolmakov <[email protected]>
AuthorDate: Tue Jul 21 12:19:02 2026 +0700

    [codegen] Support normalized key for non-compact timestamp (#8759)
---
 .../apache/paimon/codegen/SortCodeGenerator.scala  |   6 +-
 .../java/org/apache/paimon/utils/SortUtil.java     |  11 +-
 .../paimon/codegen/NormalizedKeyComputerTest.java  | 174 +++++++++++++++++++++
 3 files changed, 185 insertions(+), 6 deletions(-)

diff --git 
a/paimon-codegen/src/main/scala/org/apache/paimon/codegen/SortCodeGenerator.scala
 
b/paimon-codegen/src/main/scala/org/apache/paimon/codegen/SortCodeGenerator.scala
index 77040bd3d5..6ed773fb3b 100644
--- 
a/paimon-codegen/src/main/scala/org/apache/paimon/codegen/SortCodeGenerator.scala
+++ 
b/paimon-codegen/src/main/scala/org/apache/paimon/codegen/SortCodeGenerator.scala
@@ -410,9 +410,7 @@ class SortCodeGenerator(val input: RowType, val sortSpec: 
SortSpec) {
     t.getTypeRoot match {
       case _ if TypeUtils.isPrimitive(t) => true
       case VARCHAR | CHAR | VARBINARY | BINARY | DATE | TIME_WITHOUT_TIME_ZONE 
=> true
-      case TIMESTAMP_WITHOUT_TIME_ZONE =>
-        // TODO: support normalize key for non-compact timestamp
-        Timestamp.isCompact(t.asInstanceOf[TimestampType].getPrecision)
+      case TIMESTAMP_WITHOUT_TIME_ZONE => true
       case DECIMAL => 
Decimal.isCompact(t.asInstanceOf[DecimalType].getPrecision)
       case _ => false
     }
@@ -430,6 +428,8 @@ class SortCodeGenerator(val input: RowType, val sortSpec: 
SortSpec) {
       case TIMESTAMP_WITHOUT_TIME_ZONE
           if Timestamp.isCompact(t.asInstanceOf[TimestampType].getPrecision) =>
         8
+      // non-compact timestamp: millisecond (8) + nanoOfMillisecond (4)
+      case TIMESTAMP_WITHOUT_TIME_ZONE => 12
       case DATE => 4
       case TIME_WITHOUT_TIME_ZONE => 4
       case DECIMAL if 
Decimal.isCompact(t.asInstanceOf[DecimalType].getPrecision) => 8
diff --git a/paimon-common/src/main/java/org/apache/paimon/utils/SortUtil.java 
b/paimon-common/src/main/java/org/apache/paimon/utils/SortUtil.java
index b726532a31..5b1b6c4982 100644
--- a/paimon-common/src/main/java/org/apache/paimon/utils/SortUtil.java
+++ b/paimon-common/src/main/java/org/apache/paimon/utils/SortUtil.java
@@ -164,11 +164,16 @@ public class SortUtil {
         }
     }
 
-    /** Support the compact precision TimestampData. */
+    /** Support TimestampData of any precision. */
     public static void putTimestampNormalizedKey(
             Timestamp value, MemorySegment target, int offset, int numBytes) {
-        assert value.getNanoOfMillisecond() == 0;
-        putLongNormalizedKey(value.getMillisecond(), target, offset, numBytes);
+        // millisecond occupies the most significant 8 bytes
+        putLongNormalizedKey(value.getMillisecond(), target, offset, 
Math.min(numBytes, 8));
+        if (numBytes > 8) {
+            // nanoOfMillisecond is in [0, 999_999], so an unsigned key 
preserves order
+            putUnsignedIntegerNormalizedKey(
+                    value.getNanoOfMillisecond(), target, offset + 8, numBytes 
- 8);
+        }
     }
 
     public static void putUnsignedIntegerNormalizedKey(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/codegen/NormalizedKeyComputerTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/codegen/NormalizedKeyComputerTest.java
new file mode 100644
index 0000000000..770b19be57
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/codegen/NormalizedKeyComputerTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.paimon.codegen;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryRowWriter;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.data.serializer.AbstractRowDataSerializer;
+import org.apache.paimon.data.serializer.BinaryRowSerializer;
+import org.apache.paimon.memory.HeapMemorySegmentPool;
+import org.apache.paimon.memory.MemorySegment;
+import org.apache.paimon.memory.MemorySegmentPool;
+import org.apache.paimon.sort.BinaryInMemorySortBuffer;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.utils.MutableObjectIterator;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import static org.apache.paimon.codegen.CodeGenUtils.newNormalizedKeyComputer;
+import static org.apache.paimon.codegen.CodeGenUtils.newRecordComparator;
+import static org.apache.paimon.types.DataTypes.TIMESTAMP;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests the {@link NormalizedKeyComputer} generated by {@link 
SortCodeGenerator} for timestamps,
+ * covering non-compact precisions (sub-millisecond ordering).
+ */
+class NormalizedKeyComputerTest {
+
+    private static final int NON_COMPACT_PRECISION = 9;
+
+    @Test
+    public void testCompactTimestampKeyMetadata() {
+        NormalizedKeyComputer computer = timestampKeyComputer(3);
+        // 1 null-aware byte + 8 bytes millisecond
+        assertThat(computer.getNumKeyBytes()).isEqualTo(9);
+        assertThat(computer.isKeyFullyDetermines()).isTrue();
+    }
+
+    @Test
+    public void testNonCompactTimestampKeyMetadata() {
+        for (int precision : new int[] {4, 6, 9}) {
+            NormalizedKeyComputer computer = timestampKeyComputer(precision);
+            // 1 null-aware byte + 8 bytes millisecond + 4 bytes 
nanoOfMillisecond
+            assertThat(computer.getNumKeyBytes()).as("precision %d", 
precision).isEqualTo(13);
+            assertThat(computer.isKeyFullyDetermines()).as("precision %d", 
precision).isTrue();
+        }
+    }
+
+    @Test
+    public void testSubMillisecondOrdering() {
+        NormalizedKeyComputer computer = 
timestampKeyComputer(NON_COMPACT_PRECISION);
+        Timestamp lo = Timestamp.fromEpochMillis(1000, 111_111);
+        Timestamp hi = Timestamp.fromEpochMillis(1000, 222_222);
+
+        assertThat(normalizedCompare(computer, lo, hi)).isNegative();
+        assertThat(normalizedCompare(computer, hi, lo)).isPositive();
+        assertThat(normalizedCompare(computer, lo, lo)).isZero();
+    }
+
+    @Test
+    public void testCompareKeyMatchesCompareToForAllPairs() {
+        NormalizedKeyComputer computer = 
timestampKeyComputer(NON_COMPACT_PRECISION);
+        List<Timestamp> values = timestampSpread();
+        for (Timestamp a : values) {
+            for (Timestamp b : values) {
+                assertThat(sign(normalizedCompare(computer, a, b)))
+                        .as("compare(%s, %s)", a, b)
+                        .isEqualTo(sign(a.compareTo(b)));
+            }
+        }
+    }
+
+    @Test
+    public void testEndToEndSortByNonCompactTimestamp() throws Exception {
+        List<Timestamp> values = new ArrayList<>();
+        for (Timestamp value : timestampSpread()) {
+            // duplicate each value so equal normalized keys are exercised
+            values.add(value);
+            values.add(value);
+        }
+        Collections.shuffle(values, new Random(42));
+
+        List<Timestamp> expected = new ArrayList<>(values);
+        expected.sort(Timestamp::compareTo);
+
+        
assertThat(sortThroughBuffer(values)).containsExactlyElementsOf(expected);
+    }
+
+    private static NormalizedKeyComputer timestampKeyComputer(int precision) {
+        return newNormalizedKeyComputer(
+                Collections.singletonList(TIMESTAMP(precision)), new int[] 
{0});
+    }
+
+    private static int normalizedCompare(NormalizedKeyComputer computer, 
Timestamp a, Timestamp b) {
+        int numKeyBytes = computer.getNumKeyBytes();
+        MemorySegment segA = MemorySegment.wrap(new byte[numKeyBytes]);
+        MemorySegment segB = MemorySegment.wrap(new byte[numKeyBytes]);
+        computer.putKey(GenericRow.of(a), segA, 0);
+        computer.putKey(GenericRow.of(b), segB, 0);
+        return computer.compareKey(segA, 0, segB, 0);
+    }
+
+    private static List<Timestamp> timestampSpread() {
+        List<Timestamp> values = new ArrayList<>();
+        long[] milliseconds = {-86_400_000L, -1000L, -1L, 0L, 1L, 1000L, 
1_600_000_000_000L};
+        int[] nanos = {0, 1, 500_000, 999_999};
+        for (long millisecond : milliseconds) {
+            for (int nano : nanos) {
+                values.add(Timestamp.fromEpochMillis(millisecond, nano));
+            }
+        }
+        return values;
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    private static List<Timestamp> sortThroughBuffer(List<Timestamp> values) 
throws Exception {
+        List<DataType> fieldTypes = 
Collections.singletonList(TIMESTAMP(NON_COMPACT_PRECISION));
+        BinaryRowSerializer serializer = new BinaryRowSerializer(1);
+        MemorySegmentPool pool =
+                new HeapMemorySegmentPool(1024 * 1024, 
MemorySegmentPool.DEFAULT_PAGE_SIZE);
+        BinaryInMemorySortBuffer buffer =
+                BinaryInMemorySortBuffer.createBuffer(
+                        newNormalizedKeyComputer(fieldTypes, new int[] {0}),
+                        (AbstractRowDataSerializer) serializer,
+                        newRecordComparator(fieldTypes, new int[] {0}),
+                        pool);
+
+        BinaryRow row = new BinaryRow(1);
+        BinaryRowWriter writer = new BinaryRowWriter(row);
+        for (Timestamp value : values) {
+            writer.reset();
+            writer.writeTimestamp(0, value, NON_COMPACT_PRECISION);
+            writer.complete();
+            assertThat(buffer.write(row)).isTrue();
+        }
+
+        List<Timestamp> sorted = new ArrayList<>();
+        MutableObjectIterator<BinaryRow> iterator = buffer.sortedIterator();
+        BinaryRow reuse = serializer.createInstance();
+        BinaryRow next;
+        while ((next = iterator.next(reuse)) != null) {
+            sorted.add(next.getTimestamp(0, NON_COMPACT_PRECISION));
+        }
+        buffer.clear();
+        return sorted;
+    }
+
+    private static int sign(int value) {
+        return Integer.compare(value, 0);
+    }
+}

Reply via email to