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

wombatu-kun 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 6f2fbaaf6e4b fix(storage-format): return all records when scanning 
multi-block native HFiles (#19146)
6f2fbaaf6e4b is described below

commit 6f2fbaaf6e4bd58e8fb2c7eff9c604afc5ff25c3
Author: Y Ethan Guo <[email protected]>
AuthorDate: Thu Jul 2 22:43:52 2026 -0700

    fix(storage-format): return all records when scanning multi-block native 
HFiles (#19146)
---
 .../org/apache/hudi/io/hfile/HFileDataBlock.java   |  6 +-
 .../hudi/io/hfile/TestHFileMultiBlockScan.java     | 94 ++++++++++++++++++++++
 2 files changed, 98 insertions(+), 2 deletions(-)

diff --git a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileDataBlock.java 
b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileDataBlock.java
index cbf3e719f6a0..551cfd2c961b 100644
--- a/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileDataBlock.java
+++ b/hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileDataBlock.java
@@ -53,7 +53,9 @@ public class HFileDataBlock extends HFileBlock {
   // so the latest timestamp is used.
   private static final long LATEST_TIMESTAMP = Long.MAX_VALUE;
 
-  // End offset of content in the block, relative to the start of the start of 
the block
+  // End offset of content in the block, relative to the start of the block. 
The key-values
+  // occupy exactly uncompressedSizeWithoutHeader bytes after the header; the 
checksum trails
+  // the content and is not part of it, so it must not be subtracted here.
   protected final int uncompressedContentEndRelativeOffset;
   private final List<KeyValueEntry> entriesToWrite = new ArrayList<>();
 
@@ -64,7 +66,7 @@ public class HFileDataBlock extends HFileBlock {
     super(context, HFileBlockType.DATA, byteBuff, startOffsetInBuff);
 
     this.uncompressedContentEndRelativeOffset =
-        this.uncompressedEndOffset - this.sizeCheckSum - 
this.startOffsetInBuff;
+        this.uncompressedEndOffset - this.startOffsetInBuff;
   }
 
   // For write purpose.
diff --git 
a/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileMultiBlockScan.java 
b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileMultiBlockScan.java
new file mode 100644
index 000000000000..e36410f856b5
--- /dev/null
+++ 
b/hudi-io/src/test/java/org/apache/hudi/io/hfile/TestHFileMultiBlockScan.java
@@ -0,0 +1,94 @@
+/*
+ * 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.hudi.io.hfile;
+
+import org.apache.hudi.io.ByteArraySeekableDataInputStream;
+import org.apache.hudi.io.ByteBufferBackedInputStream;
+import org.apache.hudi.io.compress.CompressionCodec;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Locale;
+
+import static org.apache.hudi.common.util.StringUtils.getUTF8Bytes;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Reproduces a full-scan record drop on native-writer HFiles that span many 
data blocks. A large
+ * block size packs many records per block; the trailer entry count is 
correct, but a
+ * {@code seekTo()} then {@code next()} forward scan stops short of a block's 
content end because
+ * the content-end bound subtracts the trailing checksum size. The drop only 
surfaces once the
+ * checksum region exceeds the last record's size, which is why small-block 
tests never caught it.
+ */
+public class TestHFileMultiBlockScan {
+
+  @ParameterizedTest
+  @CsvSource({
+      "NONE, 64, 5000", "GZIP, 64, 5000",
+      "NONE, 1048576, 200000", "GZIP, 1048576, 200000",
+      "NONE, 65536, 200000", "GZIP, 65536, 200000"
+  })
+  public void fullScanReturnsAllRecords(String codec, int blockSize, int 
numRecords) throws Exception {
+    HFileContext context = new HFileContext.Builder()
+        .blockSize(blockSize)
+        .compressionCodec(CompressionCodec.valueOf(codec))
+        .build();
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try (HFileWriter writer = new HFileWriterImpl(context, baos)) {
+      for (int i = 0; i < numRecords; i++) {
+        writer.append(key(i), getUTF8Bytes(value(i)));
+      }
+    }
+    byte[] fileBytes = baos.toByteArray();
+
+    try (HFileReader reader = new HFileReaderImpl(
+        new ByteArraySeekableDataInputStream(new 
ByteBufferBackedInputStream(fileBytes)),
+        fileBytes.length)) {
+      reader.initializeMetadata();
+      assertEquals(numRecords, reader.getNumKeyValueEntries(),
+          "trailer entry count for codec=" + codec + " blockSize=" + 
blockSize);
+
+      String scenario = "codec=" + codec + " blockSize=" + blockSize + " 
numRecords=" + numRecords;
+      int scanned = 0;
+      int firstSkipAt = -1;
+      boolean hasNext = reader.seekTo();
+      while (hasNext) {
+        KeyValue kv = reader.getKeyValue().get();
+        if (firstSkipAt < 0 && 
!key(scanned).equals(kv.getKey().getContentInString())) {
+          firstSkipAt = scanned;
+        }
+        scanned++;
+        hasNext = reader.next();
+      }
+      assertEquals(numRecords, scanned,
+          "forward scan returned fewer records than written; " + scenario
+              + " firstSkipAtPosition=" + firstSkipAt);
+    }
+  }
+
+  private static String key(int i) {
+    return String.format(Locale.ROOT, "%010d", i);
+  }
+
+  private static String value(int i) {
+    return String.format(Locale.ROOT, "value-%010d", i);
+  }
+}

Reply via email to