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

danny0405 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 cf2566cd06c4 feat: Support insert-order record positions for native 
log files (#19200)
cf2566cd06c4 is described below

commit cf2566cd06c447ca86f5fbe45f9f3a949cd77ca2
Author: Shuo Cheng <[email protected]>
AuthorDate: Tue Jul 7 11:14:39 2026 +0800

    feat: Support insert-order record positions for native log files (#19200)
---
 .../hudi/io/cdc/HoodieNativeLogFormatWriter.java   | 24 ++----
 .../io/cdc/TestHoodieNativeLogFormatWriter.java    | 26 +++----
 .../hudi/common/table/log/LogReaderUtils.java      | 87 ++++++++++++++++++++++
 .../common/table/log/block/HoodieLogBlock.java     | 24 +++++-
 .../table/log/block/HoodieNativeLogDataBlock.java  |  5 ++
 .../log/block/HoodieNativeLogDeleteBlock.java      |  5 ++
 .../buffer/PositionBasedFileGroupRecordBuffer.java | 20 +----
 .../common/functional/TestHoodieLogFormat.java     |  4 +-
 .../hudi/common/table/log/TestLogReaderUtils.java  | 32 +++++++-
 .../hudi/functional/TestMORDataSourceStorage.scala | 21 +++---
 10 files changed, 178 insertions(+), 70 deletions(-)

diff --git 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/cdc/HoodieNativeLogFormatWriter.java
 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/cdc/HoodieNativeLogFormatWriter.java
index 09c3608a5365..ed6a0b764be0 100644
--- 
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/cdc/HoodieNativeLogFormatWriter.java
+++ 
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/io/cdc/HoodieNativeLogFormatWriter.java
@@ -24,7 +24,6 @@ import org.apache.hudi.common.fs.FSUtils;
 import org.apache.hudi.common.model.HoodieFileFormat;
 import org.apache.hudi.common.model.HoodieLogFile;
 import org.apache.hudi.common.model.HoodieRecord;
-import org.apache.hudi.common.model.HoodieRecordLocation;
 import org.apache.hudi.common.schema.HoodieSchema;
 import org.apache.hudi.common.schema.HoodieSchemas;
 import org.apache.hudi.common.table.HoodieTableVersion;
@@ -48,11 +47,9 @@ import org.apache.hudi.storage.StoragePath;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
-import java.util.Set;
 
 import static org.apache.hudi.common.model.LogExtensions.DATA_LOG_EXTENSION;
 import static org.apache.hudi.common.model.LogExtensions.DELETE_LOG_EXTENSION;
@@ -237,23 +234,12 @@ public class HoodieNativeLogFormatWriter extends 
HoodieLogFormat.Writer {
       return header;
     }
 
-    Set<Long> positionSet = new HashSet<>(recordPositions.size());
-    long previousPosition = Long.MIN_VALUE;
-    for (Long position : recordPositions) {
-      // RECORD_POSITIONS is encoded as a bitmap and decoded in ascending 
order. Native log records
-      // are read back in file write order, so only publish positions when 
both orders match.
-      if (!HoodieRecordLocation.isPositionValid(position) || position <= 
previousPosition) {
-        Map<HeaderMetadataType, String> updatedHeader = new HashMap<>(header);
-        
updatedHeader.remove(HeaderMetadataType.BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS);
-        return updatedHeader;
-      }
-      previousPosition = position;
-      positionSet.add(position);
-    }
-
     Map<HeaderMetadataType, String> updatedHeader = new HashMap<>(header);
-    if (positionSet.size() == recordPositions.size()) {
-      updatedHeader.put(HeaderMetadataType.RECORD_POSITIONS, 
LogReaderUtils.encodePositions(positionSet));
+    Option<String> encodedRecordPositions = 
LogReaderUtils.encodePositionsAsLongList(recordPositions);
+    if (encodedRecordPositions.isPresent()) {
+      updatedHeader.put(HeaderMetadataType.RECORD_POSITIONS, 
encodedRecordPositions.get());
+    } else {
+      
updatedHeader.remove(HeaderMetadataType.BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS);
     }
     return updatedHeader;
   }
diff --git 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/io/cdc/TestHoodieNativeLogFormatWriter.java
 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/io/cdc/TestHoodieNativeLogFormatWriter.java
index 7c38d7520d20..66ead8df3079 100644
--- 
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/io/cdc/TestHoodieNativeLogFormatWriter.java
+++ 
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/io/cdc/TestHoodieNativeLogFormatWriter.java
@@ -42,7 +42,6 @@ import org.apache.hudi.storage.StoragePathInfo;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.MockedStatic;
-import org.roaringbitmap.longlong.Roaring64NavigableMap;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -52,7 +51,6 @@ import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
@@ -70,15 +68,16 @@ public class TestHoodieNativeLogFormatWriter {
 
     assertEquals("001", 
parsedHeader.get(HeaderMetadataType.BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS));
     assertEquals(Arrays.asList(2L, 7L),
-        
toList(LogReaderUtils.decodeRecordPositionsHeader(parsedHeader.get(HeaderMetadataType.RECORD_POSITIONS))));
+        
LogReaderUtils.decodeRecordPositionsLongList(parsedHeader.get(HeaderMetadataType.RECORD_POSITIONS)));
   }
 
   @Test
-  public void testSkipsOutOfOrderRecordPositions() throws Exception {
+  public void testAddsOutOfOrderRecordPositionsToDataLogFooter() throws 
Exception {
     Map<HeaderMetadataType, String> parsedHeader = 
writeDataLogFooterWithPositions(7L, 2L);
 
-    
assertFalse(parsedHeader.containsKey(HeaderMetadataType.BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS));
-    assertFalse(parsedHeader.containsKey(HeaderMetadataType.RECORD_POSITIONS));
+    assertEquals("001", 
parsedHeader.get(HeaderMetadataType.BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS));
+    assertEquals(Arrays.asList(7L, 2L),
+        
LogReaderUtils.decodeRecordPositionsLongList(parsedHeader.get(HeaderMetadataType.RECORD_POSITIONS)));
   }
 
   @Test
@@ -90,11 +89,12 @@ public class TestHoodieNativeLogFormatWriter {
   }
 
   @Test
-  public void testSkipsDuplicateRecordPositions() throws Exception {
+  public void testAddsDuplicateRecordPositionsToDataLogFooter() throws 
Exception {
     Map<HeaderMetadataType, String> parsedHeader = 
writeDataLogFooterWithPositions(7L, 7L);
 
-    
assertNull(parsedHeader.get(HeaderMetadataType.BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS));
-    assertFalse(parsedHeader.containsKey(HeaderMetadataType.RECORD_POSITIONS));
+    assertEquals("001", 
parsedHeader.get(HeaderMetadataType.BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS));
+    assertEquals(Arrays.asList(7L, 7L),
+        
LogReaderUtils.decodeRecordPositionsLongList(parsedHeader.get(HeaderMetadataType.RECORD_POSITIONS)));
   }
 
   @Test
@@ -103,7 +103,7 @@ public class TestHoodieNativeLogFormatWriter {
 
     assertEquals("001", 
parsedHeader.get(HeaderMetadataType.BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS));
     assertEquals(Arrays.asList(7L),
-        
toList(LogReaderUtils.decodeRecordPositionsHeader(parsedHeader.get(HeaderMetadataType.RECORD_POSITIONS))));
+        
LogReaderUtils.decodeRecordPositionsLongList(parsedHeader.get(HeaderMetadataType.RECORD_POSITIONS)));
   }
 
   @Test
@@ -263,10 +263,4 @@ public class TestHoodieNativeLogFormatWriter {
     when(record.getOrderingValue(eq(schema), any(), 
any())).thenReturn(OrderingValues.getDefault());
     return record;
   }
-
-  private static List<Long> toList(Roaring64NavigableMap positions) {
-    List<Long> values = new ArrayList<>();
-    positions.iterator().forEachRemaining(values::add);
-    return values;
-  }
 }
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/table/log/LogReaderUtils.java
 
b/hudi-common/src/main/java/org/apache/hudi/common/table/log/LogReaderUtils.java
index 501b36c25b87..fdc69b9f13c9 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/table/log/LogReaderUtils.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/table/log/LogReaderUtils.java
@@ -21,12 +21,14 @@ package org.apache.hudi.common.table.log;
 import org.apache.hudi.common.engine.HoodieEngineContext;
 import org.apache.hudi.common.model.FileSlice;
 import org.apache.hudi.common.model.HoodieLogFile;
+import org.apache.hudi.common.model.HoodieRecordLocation;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
 import org.apache.hudi.common.table.log.block.HoodieLogBlock;
 import 
org.apache.hudi.common.table.log.block.HoodieLogBlock.HeaderMetadataType;
 import org.apache.hudi.common.table.timeline.HoodieTimeline;
 import org.apache.hudi.common.table.view.AbstractTableFileSystemView;
 import org.apache.hudi.common.util.Base64CodecUtil;
+import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.collection.Pair;
 
 import org.roaringbitmap.longlong.Roaring64NavigableMap;
@@ -35,6 +37,7 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
+import java.io.EOFException;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -152,6 +155,28 @@ public class LogReaderUtils {
     return Base64CodecUtil.encode(baos.toByteArray());
   }
 
+  /**
+   * Encodes the record positions as an integer count followed by unsigned 
varlong positions,
+   * preserving the input order.
+   *
+   * @param positions record positions in file read order.
+   * @return Base64-encoded bytes containing the list size followed by varlong 
positions,
+   * or {@link Option#empty()} if any position is invalid.
+   * @throws IOException upon I/O error.
+   */
+  public static Option<String> encodePositionsAsLongList(List<Long> positions) 
throws IOException {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream(Integer.BYTES + 
positions.size());
+    DataOutputStream dos = new DataOutputStream(baos);
+    dos.writeInt(positions.size());
+    for (Long position : positions) {
+      if (!HoodieRecordLocation.isPositionValid(position)) {
+        return Option.empty();
+      }
+      writeUnsignedVarLong(baos, position);
+    }
+    return Option.of(Base64CodecUtil.encode(baos.toByteArray()));
+  }
+
   /**
    * Decodes the {@link HeaderMetadataType#RECORD_POSITIONS} block header into 
record positions.
    *
@@ -168,4 +193,66 @@ public class LogReaderUtils {
     positionBitmap.deserializePortable(dis);
     return positionBitmap;
   }
+
+  /**
+   * Decodes native-log record positions written as an ordered unsigned 
varlong list.
+   *
+   * @param content Base64-encoded bytes containing the list size followed by 
varlong positions.
+   * @return record positions in file read order.
+   * @throws IOException upon I/O error or invalid payload shape.
+   */
+  public static List<Long> decodeRecordPositionsLongList(String content) 
throws IOException {
+    ByteArrayInputStream bais = new 
ByteArrayInputStream(Base64CodecUtil.decode(content));
+    DataInputStream dis = new DataInputStream(bais);
+    int size = dis.readInt();
+    if (size < 0) {
+      throw new IOException("Invalid negative record position count " + size);
+    }
+    List<Long> positions = new ArrayList<>(size);
+    for (int i = 0; i < size; i++) {
+      long position = readUnsignedVarLong(bais);
+      if (!HoodieRecordLocation.isPositionValid(position)) {
+        throw new IOException("Invalid record position " + position);
+      }
+      positions.add(position);
+    }
+    if (bais.available() != 0) {
+      throw new IOException("Invalid record positions payload with " + 
bais.available() + " trailing bytes");
+    }
+    return positions;
+  }
+
+  /**
+   * Writes an unsigned long by splitting it into 7-bit groups from low bits 
to high bits.
+   * The most significant bit of each byte is the continuation flag: {@code 1} 
means more
+   * bytes follow and {@code 0} marks the final byte.
+   */
+  private static void writeUnsignedVarLong(ByteArrayOutputStream out, long 
value) {
+    while ((value & 0xFFFFFFFFFFFFFF80L) != 0) {
+      out.write(((int) value & 0x7F) | 0x80);
+      value >>>= 7;
+    }
+    out.write((int) value & 0x7F);
+  }
+
+  /**
+   * Reads an unsigned long written by {@link 
#writeUnsignedVarLong(ByteArrayOutputStream, long)}
+   * by rebuilding the value from 7-bit groups in low-to-high order.
+   */
+  private static long readUnsignedVarLong(ByteArrayInputStream in) throws 
IOException {
+    long value = 0;
+    int shift = 0;
+    while (shift < Long.SIZE) {
+      int currentByte = in.read();
+      if (currentByte < 0) {
+        throw new EOFException("Unexpected EOF while reading record position");
+      }
+      value |= (long) (currentByte & 0x7F) << shift;
+      if ((currentByte & 0x80) == 0) {
+        return value;
+      }
+      shift += 7;
+    }
+    throw new IOException("Invalid unsigned varlong for record position");
+  }
 }
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieLogBlock.java
 
b/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieLogBlock.java
index 3e63daa98522..c6cb16d10dee 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieLogBlock.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieLogBlock.java
@@ -44,6 +44,8 @@ import java.io.DataOutputStream;
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InterruptedIOException;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -142,13 +144,33 @@ public abstract class HoodieLogBlock {
    * {@link Roaring64NavigableMap} bitmap.
    * @throws IOException upon I/O error.
    */
-  public Roaring64NavigableMap getRecordPositions() throws IOException {
+  private Roaring64NavigableMap getRecordPositions() throws IOException {
     if (!logBlockHeader.containsKey(HeaderMetadataType.RECORD_POSITIONS)) {
       return new Roaring64NavigableMap();
     }
     return 
LogReaderUtils.decodeRecordPositionsHeader(logBlockHeader.get(HeaderMetadataType.RECORD_POSITIONS));
   }
 
+  /**
+   * @return record positions aligned with the block record iterator order.
+   * @throws IOException upon I/O error.
+   */
+  public List<Long> getRecordPositionList() throws IOException {
+    List<Long> positions = new ArrayList<>();
+    getRecordPositions().iterator().forEachRemaining(positions::add);
+    return positions;
+  }
+
+  /**
+   * Decodes ordered record positions stored for native log blocks.
+   */
+  protected static List<Long> 
decodeOrderedRecordPositionList(Map<HeaderMetadataType, String> logBlockHeader) 
throws IOException {
+    if (!logBlockHeader.containsKey(HeaderMetadataType.RECORD_POSITIONS)) {
+      return Collections.emptyList();
+    }
+    return 
LogReaderUtils.decodeRecordPositionsLongList(logBlockHeader.get(HeaderMetadataType.RECORD_POSITIONS));
+  }
+
   /**
    * @return base file instant time of the record positions if the record 
positions are enabled
    * in the log block; {@code null} otherwise.
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieNativeLogDataBlock.java
 
b/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieNativeLogDataBlock.java
index 673707e3f920..c4b0ed294028 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieNativeLogDataBlock.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieNativeLogDataBlock.java
@@ -69,6 +69,11 @@ public class HoodieNativeLogDataBlock extends 
HoodieDataBlock {
     return HoodieLogBlockType.PARQUET_DATA_BLOCK;
   }
 
+  @Override
+  public List<Long> getRecordPositionList() throws IOException {
+    return decodeOrderedRecordPositionList(getLogBlockHeader());
+  }
+
   @Override
   protected <T> ClosableIterator<HoodieRecord<T>> 
readRecordsFromBlockPayload(HoodieRecord.HoodieRecordType type) throws 
IOException {
     StoragePath path = logFile.getPath();
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieNativeLogDeleteBlock.java
 
b/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieNativeLogDeleteBlock.java
index ddaeab29760e..031bbe609929 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieNativeLogDeleteBlock.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/table/log/block/HoodieNativeLogDeleteBlock.java
@@ -75,6 +75,11 @@ public class HoodieNativeLogDeleteBlock extends 
HoodieDeleteBlock {
     this.props = props == null ? new Properties() : props;
   }
 
+  @Override
+  public List<Long> getRecordPositionList() throws IOException {
+    return decodeOrderedRecordPositionList(getLogBlockHeader());
+  }
+
   @Override
   public DeleteRecord[] getRecordsToDelete() {
     if (recordsToDelete == null) {
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/table/read/buffer/PositionBasedFileGroupRecordBuffer.java
 
b/hudi-common/src/main/java/org/apache/hudi/common/table/read/buffer/PositionBasedFileGroupRecordBuffer.java
index 2ca86d6ad7a5..d3c49801e891 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/common/table/read/buffer/PositionBasedFileGroupRecordBuffer.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/common/table/read/buffer/PositionBasedFileGroupRecordBuffer.java
@@ -42,13 +42,11 @@ import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieKeyException;
 
 import lombok.extern.slf4j.Slf4j;
-import org.roaringbitmap.longlong.Roaring64NavigableMap;
 
 import java.io.IOException;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Function;
@@ -170,7 +168,7 @@ public class PositionBasedFileGroupRecordBuffer<T> extends 
KeyBasedFileGroupReco
         records.remove(position);
       } else {
         //if it's a delete record and the key is null, then we need to still 
use positions
-        //this happens when we read the positions using 
logBlock.getRecordPositions()
+        //this happens when we read the positions using 
logBlock.getRecordPositionList()
         //instead of reading the delete records themselves
         needToDoHybridStrategy = true;
       }
@@ -296,8 +294,6 @@ public class PositionBasedFileGroupRecordBuffer<T> extends 
KeyBasedFileGroupReco
    */
   protected static List<Long> extractRecordPositions(HoodieLogBlock logBlock,
                                                      String 
baseFileInstantTime) throws IOException {
-    List<Long> blockPositions = new ArrayList<>();
-
     String blockBaseFileInstantTime = 
logBlock.getBaseFileInstantTimeOfPositions();
     if (StringUtils.isNullOrEmpty(blockBaseFileInstantTime) || 
!baseFileInstantTime.equals(blockBaseFileInstantTime)) {
       log.debug("The record positions cannot be used because the base file 
instant time "
@@ -306,19 +302,9 @@ public class PositionBasedFileGroupRecordBuffer<T> extends 
KeyBasedFileGroupReco
           blockBaseFileInstantTime, baseFileInstantTime);
       return null;
     }
-    Roaring64NavigableMap positions = logBlock.getRecordPositions();
-    if (positions == null || positions.isEmpty()) {
-      log.info("No record position info is found when attempting to do 
position based merge.");
-      return null;
-    }
-
-    Iterator<Long> iterator = positions.iterator();
-    while (iterator.hasNext()) {
-      blockPositions.add(iterator.next());
-    }
-
+    List<Long> blockPositions = logBlock.getRecordPositionList();
     if (blockPositions.isEmpty()) {
-      log.info("No positions are extracted.");
+      log.info("No record position info is found when attempting to do 
position based merge.");
       return null;
     }
 
diff --git 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/functional/TestHoodieLogFormat.java
 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/functional/TestHoodieLogFormat.java
index 6efc1efbd498..f4b3732a17a9 100755
--- 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/functional/TestHoodieLogFormat.java
+++ 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/functional/TestHoodieLogFormat.java
@@ -3034,9 +3034,9 @@ public class TestHoodieLogFormat extends 
HoodieCommonTestHarness {
     String expectedInstantTime = hasPositions ? "001" : null;
     Set<Long> expectedPositions = hasPositions ? new HashSet<>(positions) : 
Collections.emptySet();
     assertEquals(expectedInstantTime, 
deleteBlock.getBaseFileInstantTimeOfPositions());
-    TestLogReaderUtils.assertPositionEquals(expectedPositions, 
deleteBlock.getRecordPositions());
+    assertEquals(expectedPositions, new 
HashSet<>(deleteBlock.getRecordPositionList()));
     assertEquals(expectedInstantTime, 
dataBlock.getBaseFileInstantTimeOfPositions());
-    TestLogReaderUtils.assertPositionEquals(expectedPositions, 
dataBlock.getRecordPositions());
+    assertEquals(expectedPositions, new 
HashSet<>(dataBlock.getRecordPositionList()));
   }
 
   private static Stream<Arguments> testArguments() {
diff --git 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/table/log/TestLogReaderUtils.java
 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/table/log/TestLogReaderUtils.java
index 6e65ef3c656e..edb7ec42638c 100644
--- 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/common/table/log/TestLogReaderUtils.java
+++ 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/common/table/log/TestLogReaderUtils.java
@@ -19,6 +19,9 @@
 
 package org.apache.hudi.common.table.log;
 
+import org.apache.hudi.common.util.Base64CodecUtil;
+import org.apache.hudi.common.util.Option;
+
 import org.junit.jupiter.api.Test;
 import org.roaringbitmap.longlong.Roaring64NavigableMap;
 
@@ -34,6 +37,7 @@ import java.util.stream.Collectors;
 import static 
org.apache.hudi.common.testutils.HoodieTestTable.readLastLineFromResourceFile;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Tests for {@link LogReaderUtils}
@@ -67,6 +71,22 @@ public class TestLogReaderUtils {
     assertPositionEquals(expectedPositions, roaring64NavigableMap);
   }
 
+  @Test
+  public void testEncodeAndDecodePositionsAsOrderedLongList() throws 
IOException {
+    List<Long> positions = Arrays.asList(7L, 2L, 7L, 128L, 16_384L);
+
+    Option<String> content = 
LogReaderUtils.encodePositionsAsLongList(positions);
+
+    assertTrue(content.isPresent());
+    assertEquals(positions, 
LogReaderUtils.decodeRecordPositionsLongList(content.get()));
+    assertTrue(Base64CodecUtil.decode(content.get()).length < Integer.BYTES + 
positions.size() * Long.BYTES);
+  }
+
+  @Test
+  public void testSkipEncodingInvalidPositionInOrderedLongList() throws 
IOException {
+    assertFalse(LogReaderUtils.encodePositionsAsLongList(Arrays.asList(7L, 
-1L)).isPresent());
+  }
+
   public static Set<Long> generatePositions() {
     Random random = new Random(0x2023);
     Set<Long> positions = new HashSet<>();
@@ -81,12 +101,16 @@ public class TestLogReaderUtils {
                                           Roaring64NavigableMap 
roaring64NavigableMap) {
     List<Long> sortedExpectedPositions =
         expectedPositions.stream().sorted().collect(Collectors.toList());
+    assertPositionListEquals(sortedExpectedPositions, 
roaring64NavigableMap.iterator());
+  }
+
+  private static void assertPositionListEquals(List<Long> 
sortedExpectedPositions,
+                                               Iterator<Long> actualIterator) {
     Iterator<Long> expectedIterator = sortedExpectedPositions.iterator();
-    Iterator<Long> iterator = roaring64NavigableMap.iterator();
-    while (expectedIterator.hasNext() && iterator.hasNext()) {
-      assertEquals(expectedIterator.next(), iterator.next());
+    while (expectedIterator.hasNext() && actualIterator.hasNext()) {
+      assertEquals(expectedIterator.next(), actualIterator.next());
     }
     assertFalse(expectedIterator.hasNext());
-    assertFalse(iterator.hasNext());
+    assertFalse(actualIterator.hasNext());
   }
 }
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSourceStorage.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSourceStorage.scala
index 2b74a7bcb0cd..ce67270b0181 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSourceStorage.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/functional/TestMORDataSourceStorage.scala
@@ -362,17 +362,16 @@ class TestMORDataSourceStorage extends 
SparkClientFunctionalTestHarness {
         var numBlocks = 0
         while (logFormatReader.hasNext) {
           val logBlock = logFormatReader.next()
-          // todo(https://github.com/apache/hudi/issues/19178), re-enable the 
follow test after the issue solved.
-          // val recordPositions = logBlock.getRecordPositions
-          //
-          // assertEquals(shouldContainRecordPosition, 
!recordPositions.isEmpty)
-          // if (shouldContainRecordPosition) {
-          //  val baseFile = fsv.getLatestBaseFile("", filename.getFileId)
-          //  assertTrue(baseFile.isPresent)
-          //   assertEquals(
-          //    shouldBaseFileInstantTimeMatch,
-          //    
baseFile.get().getCommitTime.equals(logBlock.getBaseFileInstantTimeOfPositions))
-          // }
+          val recordPositions = logBlock.getRecordPositionList
+
+          assertEquals(shouldContainRecordPosition, !recordPositions.isEmpty)
+          if (shouldContainRecordPosition) {
+            val baseFile = fsv.getLatestBaseFile("", filename.getFileId)
+            assertTrue(baseFile.isPresent)
+            assertEquals(
+              shouldBaseFileInstantTimeMatch,
+              
baseFile.get().getCommitTime.equals(logBlock.getBaseFileInstantTimeOfPositions))
+          }
           numBlocks += 1
         }
         assertTrue(numBlocks > 0)

Reply via email to