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

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


The following commit(s) were added to refs/heads/master by this push:
     new dfc210631e99 fix(flink): use UTF-8 ordering for LSM record keys 
(#19548)
dfc210631e99 is described below

commit dfc210631e999da6e1c0f3d64fbec94aec660441
Author: Shuo Cheng <[email protected]>
AuthorDate: Tue Aug 11 14:05:16 2026 +0800

    fix(flink): use UTF-8 ordering for LSM record keys (#19548)
---
 .../read/lsm/TestLsmFileGroupRecordIterator.java   | 13 ++--
 .../hudi/sink/utils/RecordKeySortComparator.java   | 11 ++-
 .../hudi/sink/utils/RecordKeySortKeyComputer.java  | 68 +++++++++++++++--
 .../sink/utils/TestRecordKeySortKeyComputer.java   | 86 ++++++++++++++++------
 4 files changed, 138 insertions(+), 40 deletions(-)

diff --git 
a/hudi-common/src/test/java/org/apache/hudi/common/table/read/lsm/TestLsmFileGroupRecordIterator.java
 
b/hudi-common/src/test/java/org/apache/hudi/common/table/read/lsm/TestLsmFileGroupRecordIterator.java
index edd5a83284b4..0bd825460b40 100644
--- 
a/hudi-common/src/test/java/org/apache/hudi/common/table/read/lsm/TestLsmFileGroupRecordIterator.java
+++ 
b/hudi-common/src/test/java/org/apache/hudi/common/table/read/lsm/TestLsmFileGroupRecordIterator.java
@@ -198,17 +198,18 @@ class TestLsmFileGroupRecordIterator {
 
   @Test
   void testLoserTreeUsesUtf8Ordering() {
-    String bmpPrivateUseKey = new String(Character.toChars(0xE000));
-    String supplementaryKey = new String(Character.toChars(0x20000));
+    String fullWidthExclamationKey = "!";
+    String emojiKey = "😀";
 
     LsmFileGroupRecordIterator.LoserTree<String> loserTree =
         new LsmFileGroupRecordIterator.LoserTree<>(
             Arrays.asList(
-                sortedRunReader(0, record(bmpPrivateUseKey, "bmp")),
-                sortedRunReader(1, record(supplementaryKey, 
"supplementary"))));
+                sortedRunReader(0, record(fullWidthExclamationKey, 
"full-width-exclamation")),
+                sortedRunReader(1, record(emojiKey, "emoji"))));
+    // UTF-16 orders the emoji first, while UTF-8 bytes order the full-width 
character first.
     assertEquals(Arrays.asList(
-        bmpPrivateUseKey + ":bmp",
-        supplementaryKey + ":supplementary"), drain(loserTree));
+        fullWidthExclamationKey + ":full-width-exclamation",
+        emojiKey + ":emoji"), drain(loserTree));
   }
 
   @Test
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/RecordKeySortComparator.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/RecordKeySortComparator.java
index 1abd372ab5c6..305f7f1903e8 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/RecordKeySortComparator.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/RecordKeySortComparator.java
@@ -18,6 +18,7 @@
 
 package org.apache.hudi.sink.utils;
 
+import org.apache.hudi.common.util.StringUtils;
 import org.apache.hudi.sink.bulk.RowDataKeyGen;
 
 import org.apache.flink.table.data.RowData;
@@ -26,9 +27,9 @@ import 
org.apache.flink.table.runtime.generated.RecordComparator;
 /**
  * Compares rows by compact keys that preserve encoded Hudi record-key 
ordering.
  *
- * <p>LSM sorted runs must use the same ordering as the common LSM reader, 
which compares the
- * encoded record-key strings. Comparing the typed primary-key fields directly 
is not equivalent;
- * for example, integer keys {@code 2} and {@code 10} have the opposite order 
after encoding.
+ * <p>LSM sorted runs must use the same unsigned UTF-8 byte ordering as the 
common LSM reader.
+ * Comparing the typed primary-key fields directly is not equivalent; for 
example, integer keys
+ * {@code 2} and {@code 10} have the opposite order after encoding.
  */
 public class RecordKeySortComparator implements RecordComparator {
   private final RowDataKeyGen keyGen;
@@ -39,6 +40,8 @@ public class RecordKeySortComparator implements 
RecordComparator {
 
   @Override
   public int compare(RowData record1, RowData record2) {
-    return 
keyGen.getRecordKeyForComparison(record1).compareTo(keyGen.getRecordKeyForComparison(record2));
+    return StringUtils.compareUtf8Bytes(
+        keyGen.getRecordKeyForComparison(record1),
+        keyGen.getRecordKeyForComparison(record2));
   }
 }
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/RecordKeySortKeyComputer.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/RecordKeySortKeyComputer.java
index 932167b4dede..2fdb7053699d 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/RecordKeySortKeyComputer.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/RecordKeySortKeyComputer.java
@@ -20,7 +20,6 @@ package org.apache.hudi.sink.utils;
 
 import org.apache.hudi.sink.bulk.RowDataKeyGen;
 
-import org.apache.flink.api.common.typeutils.base.StringComparator;
 import org.apache.flink.core.memory.MemorySegment;
 import org.apache.flink.table.data.RowData;
 import org.apache.flink.table.runtime.generated.NormalizedKeyComputer;
@@ -28,10 +27,9 @@ import 
org.apache.flink.table.runtime.generated.NormalizedKeyComputer;
 /**
  * Computes a normalized prefix for sorting rows by their encoded Hudi record 
key.
  *
- * <p>The common LSM reader compares encoded record keys with {@link 
String#compareTo(String)},
- * whose order is based on UTF-16 code units. This computer therefore uses 
Flink's normalized-key
- * encoding for Java strings, which preserves that order while still using 
only one byte for the
- * common ASCII case.
+ * <p>The common LSM reader compares encoded record keys by their unsigned 
UTF-8 byte
+ * representation. This computer copies a prefix of those bytes into the 
normalized key so its
+ * ordering is consistent with the reader.
  *
  * <p>The normalized prefix is eight bytes for one record-key field and 
sixteen bytes for two or
  * more fields. It is packed into one or two big-endian {@code long}s so the 
sort hot path can
@@ -47,7 +45,6 @@ public class RecordKeySortKeyComputer implements 
NormalizedKeyComputer {
 
   private final RowDataKeyGen keyGen;
   private final int numKeyBytes;
-  private final StringComparator stringComparator = new StringComparator(true);
 
   /**
    * Creates a normalized-key computer using up to sixteen bytes of the 
encoded record-key prefix.
@@ -62,8 +59,65 @@ public class RecordKeySortKeyComputer implements 
NormalizedKeyComputer {
 
   @Override
   public void putKey(RowData record, MemorySegment target, int offset) {
-    stringComparator.putNormalizedKey(
+    int bytesWritten = putUtf8Prefix(
         keyGen.getRecordKeyForComparison(record), target, offset, numKeyBytes);
+    for (int i = bytesWritten; i < numKeyBytes; i++) {
+      target.put(offset + i, (byte) 0);
+    }
+  }
+
+  /**
+   * Encodes at most {@code maxBytes} of the UTF-8 prefix directly into the 
target segment.
+   *
+   * <p>This writes the exact first {@code maxBytes} UTF-8 bytes, including a 
partial multi-byte
+   * sequence at the prefix boundary, without materializing the complete 
encoded record key.
+   *
+   * <p>Production record keys contain well-formed UTF-16 because they are 
derived from UTF-8 input.
+   * An unpaired surrogate is encoded with the same replacement byte used by
+   * {@link String#getBytes(java.nio.charset.Charset)}.
+   */
+  private static int putUtf8Prefix(
+      String value, MemorySegment target, int offset, int maxBytes) {
+    int bytesWritten = 0;
+    for (int i = 0; i < value.length() && bytesWritten < maxBytes; i++) {
+      char current = value.charAt(i);
+      int encoded;
+      int encodedBytes;
+      // Pack the encoded bytes in big-endian order into the least-significant 
encodedBytes bytes.
+      if (current < 0x80) {
+        encoded = current;
+        encodedBytes = 1;
+      } else if (current < 0x800) {
+        encoded = (0xC0 | current >>> 6) << 8
+            | (0x80 | current & 0x3F);
+        encodedBytes = 2;
+      } else if (Character.isHighSurrogate(current)
+          && i + 1 < value.length()
+          && Character.isLowSurrogate(value.charAt(i + 1))) {
+        int codePoint = Character.toCodePoint(current, value.charAt(++i));
+        encoded = (0xF0 | codePoint >>> 18) << 24
+            | (0x80 | codePoint >>> 12 & 0x3F) << 16
+            | (0x80 | codePoint >>> 6 & 0x3F) << 8
+            | (0x80 | codePoint & 0x3F);
+        encodedBytes = 4;
+      } else if (Character.isSurrogate(current)) {
+        encoded = '?';
+        encodedBytes = 1;
+      } else {
+        encoded = (0xE0 | current >>> 12) << 16
+            | (0x80 | current >>> 6 & 0x3F) << 8
+            | (0x80 | current & 0x3F);
+        encodedBytes = 3;
+      }
+
+      // Writing byte by byte is intentional: the normalized prefix may end 
inside this sequence.
+      for (int shift = (encodedBytes - 1) * Byte.SIZE;
+           shift >= 0 && bytesWritten < maxBytes;
+           shift -= Byte.SIZE) {
+        target.put(offset + bytesWritten++, (byte) (encoded >>> shift));
+      }
+    }
+    return bytesWritten;
   }
 
   @Override
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestRecordKeySortKeyComputer.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestRecordKeySortKeyComputer.java
index 8bb29ceabb2e..fb0d998448f5 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestRecordKeySortKeyComputer.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestRecordKeySortKeyComputer.java
@@ -18,6 +18,7 @@
 
 package org.apache.hudi.sink.utils;
 
+import org.apache.hudi.common.util.StringUtils;
 import org.apache.hudi.configuration.FlinkOptions;
 import org.apache.hudi.sink.buffer.HeapMemorySegmentPool;
 import org.apache.hudi.sink.bulk.RowDataKeyGen;
@@ -40,12 +41,13 @@ import org.apache.flink.table.types.logical.VarCharType;
 import org.apache.flink.util.MutableObjectIterator;
 import org.junit.jupiter.api.Test;
 
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Random;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -85,7 +87,7 @@ class TestRecordKeySortKeyComputer {
   }
 
   @Test
-  void testNormalizedKeyPreservesJavaStringOrder() {
+  void testNormalizedKeyPreservesUtf8Order() {
     RowType rowType = rowType(new String[] {"key"}, new LogicalType[] {new 
VarCharType()});
     RowDataKeyGen keyGen = keyGen(rowType, "key");
     RecordKeySortKeyComputer computer = new RecordKeySortKeyComputer(keyGen, 
1);
@@ -94,15 +96,44 @@ class TestRecordKeySortKeyComputer {
     assertComparison(computer, comparator, stringRow("10"), stringRow("2"), 
false);
     assertComparison(computer, comparator, stringRow("abc"), 
stringRow("abcd"), false);
     assertComparison(computer, comparator, stringRow("\u0000a"), 
stringRow("\u0000b"), false);
-    String supplementaryCharacter = new String(Character.toChars(0x1F600));
-    String privateUseCharacter = String.valueOf((char) 0xE000);
+    String supplementaryCharacter = new String(Character.toChars(0x20000));
+    String privateUseCharacter = new String(Character.toChars(0xE000));
+    assertTrue(StringUtils.compareUtf8Bytes(privateUseCharacter, 
supplementaryCharacter) < 0);
     assertComparison(computer, comparator,
-        stringRow(supplementaryCharacter), stringRow(privateUseCharacter), 
false);
+        stringRow(privateUseCharacter), stringRow(supplementaryCharacter), 
false);
     assertComparison(computer, comparator, stringRow("abcdefgh1"), 
stringRow("abcdefgh2"), true);
   }
 
   @Test
-  void testNormalizedKeyCompareMatchesStringCompareToAtEncodingBoundaries() {
+  void testNormalizedKeyContainsExactUtf8Prefix() {
+    RowType rowType = rowType(new String[] {"key"}, new LogicalType[] {new 
VarCharType()});
+    RowDataKeyGen keyGen = keyGen(rowType, "key");
+    RecordKeySortKeyComputer computer = new RecordKeySortKeyComputer(keyGen, 
1);
+    List<String> values = Arrays.asList(
+        "ascii",
+        "12345678suffix",
+        "1234567" + new String(Character.toChars(0x20000)),
+        "123456" + new String(Character.toChars(0x20000)),
+        stringFromCodeUnits(0x007F, 0x0080, 0x07FF, 0x0800),
+        new String(Character.toChars(0xE000)),
+        new String(Character.toChars(0x10FFFF)));
+
+    for (String value : values) {
+      MemorySegment normalizedKey =
+          
MemorySegmentFactory.allocateUnpooledSegment(computer.getNumKeyBytes());
+      computer.putKey(stringRow(value), normalizedKey, 0);
+
+      byte[] expected = Arrays.copyOf(
+          
keyGen.getRecordKeyForComparison(stringRow(value)).getBytes(StandardCharsets.UTF_8),
+          computer.getNumKeyBytes());
+      byte[] actual = new byte[computer.getNumKeyBytes()];
+      normalizedKey.get(0, actual);
+      assertArrayEquals(expected, actual, () -> "Unexpected UTF-8 prefix for " 
+ printable(value));
+    }
+  }
+
+  @Test
+  void testNormalizedKeyCompareMatchesUtf8OrderAtEncodingBoundaries() {
     List<String> values = Arrays.asList(
         stringFromCodeUnits(0x0000),
         stringFromCodeUnits(0x0000) + "a",
@@ -116,26 +147,29 @@ class TestRecordKeySortKeyComputer {
         stringFromCodeUnits(0x1FFF),
         stringFromCodeUnits(0x2000),
         stringFromCodeUnits(0xD7FF),
-        stringFromCodeUnits(0xD800),
         stringFromCodeUnits(0xD800, 0xDC00),
         stringFromCodeUnits(0xD83D, 0xDE00),
         stringFromCodeUnits(0xE000),
         stringFromCodeUnits(0xFFFF));
 
-    assertNormalizedKeyComparisonsMatchStringCompareTo(values);
+    assertNormalizedKeyComparisonsMatchUtf8Order(values);
   }
 
   @Test
-  void testNormalizedKeyCompareMatchesStringCompareToForRandomUtf16Strings() {
+  void testNormalizedKeyCompareMatchesUtf8OrderForRandomStrings() {
     Random random = new Random(42);
     List<String> values = new ArrayList<>();
     for (int i = 0; i < 1_000; i++) {
       int length = random.nextInt(23) + 1;
-      char[] chars = new char[length];
+      StringBuilder builder = new StringBuilder();
       for (int j = 0; j < length; j++) {
-        chars[j] = (char) random.nextInt(Character.MAX_VALUE + 1);
+        int codePoint;
+        do {
+          codePoint = random.nextInt(Character.MAX_CODE_POINT + 1);
+        } while (codePoint >= Character.MIN_SURROGATE && codePoint <= 
Character.MAX_SURROGATE);
+        builder.appendCodePoint(codePoint);
       }
-      values.add(new String(chars));
+      values.add(builder.toString());
     }
 
     RowType rowType = rowType(new String[] {"key"}, new LogicalType[] {new 
VarCharType()});
@@ -145,7 +179,7 @@ class TestRecordKeySortKeyComputer {
     for (int i = 0; i < values.size(); i++) {
       String left = values.get(i);
       String right = values.get((i * 31 + 17) % values.size());
-      assertNormalizedComparisonMatchesStringCompareTo(
+      assertNormalizedComparisonMatchesUtf8Order(
           computer, comparator, keyGen, left, right);
     }
   }
@@ -161,14 +195,17 @@ class TestRecordKeySortKeyComputer {
         stringRow("aa", "a"),
         stringRow("a,b", "c"),
         stringRow("a", "b,c"),
+        stringRow(new String(Character.toChars(0xE000)), "a"),
+        stringRow(new String(Character.toChars(0x20000)), "a"),
         stringRow("", "a"),
         GenericRowData.of(null, StringData.fromString("a")));
 
     for (RowData left : rows) {
       for (RowData right : rows) {
-        int recordKeyResult = 
keyGen.getRecordKey(left).compareTo(keyGen.getRecordKey(right));
-        int comparisonKeyResult = keyGen.getRecordKeyForComparison(left)
-            .compareTo(keyGen.getRecordKeyForComparison(right));
+        int recordKeyResult = StringUtils.compareUtf8Bytes(
+            keyGen.getRecordKey(left), keyGen.getRecordKey(right));
+        int comparisonKeyResult = StringUtils.compareUtf8Bytes(
+            keyGen.getRecordKeyForComparison(left), 
keyGen.getRecordKeyForComparison(right));
         assertEquals(Integer.signum(recordKeyResult), 
Integer.signum(comparisonKeyResult));
       }
     }
@@ -186,7 +223,9 @@ class TestRecordKeySortKeyComputer {
     assertBufferSort(complexRowType, "key1,key2", Arrays.asList(
         stringRow("same-prefix-2", "a"),
         stringRow("same-prefix-1", "z"),
-        stringRow("same-prefix-1", "a")));
+        stringRow("same-prefix-1", "a"),
+        stringRow(new String(Character.toChars(0xE000)), "a"),
+        stringRow(new String(Character.toChars(0x20000)), "a")));
   }
 
   private static void assertComparison(
@@ -207,20 +246,20 @@ class TestRecordKeySortKeyComputer {
     assertEquals(Integer.signum(comparator.compare(left, right)), 
Integer.signum(result));
   }
 
-  private static void 
assertNormalizedKeyComparisonsMatchStringCompareTo(List<String> values) {
+  private static void 
assertNormalizedKeyComparisonsMatchUtf8Order(List<String> values) {
     RowType rowType = rowType(new String[] {"key"}, new LogicalType[] {new 
VarCharType()});
     RowDataKeyGen keyGen = keyGen(rowType, "key");
     RecordKeySortKeyComputer computer = new RecordKeySortKeyComputer(keyGen, 
1);
     RecordKeySortComparator comparator = new RecordKeySortComparator(keyGen);
     for (String left : values) {
       for (String right : values) {
-        assertNormalizedComparisonMatchesStringCompareTo(
+        assertNormalizedComparisonMatchesUtf8Order(
             computer, comparator, keyGen, left, right);
       }
     }
   }
 
-  private static void assertNormalizedComparisonMatchesStringCompareTo(
+  private static void assertNormalizedComparisonMatchesUtf8Order(
       RecordKeySortKeyComputer computer,
       RecordKeySortComparator comparator,
       RowDataKeyGen keyGen,
@@ -234,8 +273,9 @@ class TestRecordKeySortKeyComputer {
     computer.putKey(leftRow, leftKey, 0);
     computer.putKey(rightRow, rightKey, 0);
 
-    int expected = keyGen.getRecordKeyForComparison(leftRow)
-        .compareTo(keyGen.getRecordKeyForComparison(rightRow));
+    int expected = StringUtils.compareUtf8Bytes(
+        keyGen.getRecordKeyForComparison(leftRow),
+        keyGen.getRecordKeyForComparison(rightRow));
     int normalizedResult = computer.compareKey(leftKey, 0, rightKey, 0);
     if (normalizedResult != 0) {
       assertEquals(Integer.signum(expected), Integer.signum(normalizedResult),
@@ -279,7 +319,7 @@ class TestRecordKeySortKeyComputer {
         assertTrue(buffer.write(row));
         expected.add(keyGen.getRecordKey(row));
       }
-      Collections.sort(expected);
+      expected.sort(StringUtils.UTF8_LEXICOGRAPHIC_COMPARATOR);
 
       new QuickSort().sort(buffer);
       MutableObjectIterator<BinaryRowData> iterator = buffer.getIterator();

Reply via email to