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 2de099c97269 feat: Support read and write of HFile footer metadata 
(#19129)
2de099c97269 is described below

commit 2de099c972695ad4d15d08f7478504eaa5edd4ff
Author: Shuo Cheng <[email protected]>
AuthorDate: Wed Jul 1 13:53:54 2026 +0800

    feat: Support read and write of HFile footer metadata (#19129)
    
    * feat: Support read and write of HFile footer metadata
---
 .../org/apache/hudi/common/util/HFileUtils.java    | 26 +++++++++++++++++++++-
 .../io/storage/hadoop/HoodieAvroHFileWriter.java   |  6 +++++
 .../io/hadoop/TestHoodieHFileReaderWriter.java     | 24 ++++++++++++++++++++
 3 files changed, 55 insertions(+), 1 deletion(-)

diff --git 
a/hudi-common/src/main/java/org/apache/hudi/common/util/HFileUtils.java 
b/hudi-common/src/main/java/org/apache/hudi/common/util/HFileUtils.java
index 864bd76e2b14..be74096edd63 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/util/HFileUtils.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/util/HFileUtils.java
@@ -34,10 +34,14 @@ import 
org.apache.hudi.common.util.collection.ClosableIterator;
 import org.apache.hudi.common.util.collection.Pair;
 import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieIOException;
+import org.apache.hudi.exception.MetadataNotFoundException;
 import org.apache.hudi.io.compress.CompressionCodec;
 import org.apache.hudi.io.hfile.HFileContext;
+import org.apache.hudi.io.hfile.HFileReader;
 import org.apache.hudi.io.hfile.HFileWriter;
 import org.apache.hudi.io.hfile.HFileWriterImpl;
+import org.apache.hudi.io.hfile.UTF8StringKey;
+import org.apache.hudi.io.storage.HFileReaderFactory;
 import org.apache.hudi.io.storage.HoodieAvroHFileReaderImplBase;
 import org.apache.hudi.io.storage.HoodieFileReader;
 import org.apache.hudi.io.storage.HoodieIOFactory;
@@ -54,6 +58,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -66,6 +71,7 @@ import static 
org.apache.hudi.common.config.HoodieStorageConfig.BLOOM_FILTER_NUM
 import static 
org.apache.hudi.common.config.HoodieStorageConfig.BLOOM_FILTER_TYPE;
 import static 
org.apache.hudi.common.config.HoodieStorageConfig.HFILE_COMPRESSION_ALGORITHM_NAME;
 import static 
org.apache.hudi.common.config.HoodieStorageConfig.HFILE_WITH_BLOOM_FILTER_ENABLED;
+import static org.apache.hudi.common.util.StringUtils.fromUTF8Bytes;
 import static org.apache.hudi.common.util.StringUtils.getUTF8Bytes;
 
 /**
@@ -101,7 +107,25 @@ public class HFileUtils extends FileFormatUtils {
 
   @Override
   public Map<String, String> readFooter(HoodieStorage storage, boolean 
required, StoragePath filePath, String... footerNames) {
-    throw new UnsupportedOperationException("HFileUtils does not support 
readFooter");
+    Map<String, String> footerVals = new HashMap<>();
+    try (HFileReader reader = HFileReaderFactory.builder()
+        .withStorage(storage)
+        .withPath(filePath)
+        .build()
+        .createHFileReader()) {
+      for (String footerName : footerNames) {
+        Option<byte[]> footerValue = reader.getMetaInfo(new 
UTF8StringKey(footerName));
+        if (footerValue.isPresent()) {
+          footerVals.put(footerName, fromUTF8Bytes(footerValue.get()));
+        } else if (required) {
+          throw new MetadataNotFoundException(
+              "Could not find metadata key in HFile footer. Key " + footerName 
+ ", file: " + filePath);
+        }
+      }
+      return footerVals;
+    } catch (IOException io) {
+      throw new HoodieIOException("Unable to read footer for HFile: " + 
filePath, io);
+    }
   }
 
   @Override
diff --git 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieAvroHFileWriter.java
 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieAvroHFileWriter.java
index 15f088cbd8aa..a74aa5aa571c 100644
--- 
a/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieAvroHFileWriter.java
+++ 
b/hudi-hadoop-common/src/main/java/org/apache/hudi/io/storage/hadoop/HoodieAvroHFileWriter.java
@@ -49,6 +49,7 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicLong;
 
 import static org.apache.hudi.common.util.StringUtils.EMPTY_STRING;
@@ -128,6 +129,11 @@ public class HoodieAvroHFileWriter
     return !isWrapperFileSystem || wrapperFs.get().getBytesWritten(file) < 
maxFileSize;
   }
 
+  @Override
+  public void addFooterMetadata(Map<String, String> footerMetadata) {
+    footerMetadata.forEach((key, value) -> writer.appendFileInfo(key, 
getUTF8Bytes(value)));
+  }
+
   @Override
   public void writeAvro(String recordKey, IndexedRecord record) throws 
IOException {
     if (!this.hfileConfig.isAllowDuplicatesOnHfileWrites()) {
diff --git 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/io/hadoop/TestHoodieHFileReaderWriter.java
 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/io/hadoop/TestHoodieHFileReaderWriter.java
index f2225ff3ca66..9029085df4db 100644
--- 
a/hudi-hadoop-common/src/test/java/org/apache/hudi/io/hadoop/TestHoodieHFileReaderWriter.java
+++ 
b/hudi-hadoop-common/src/test/java/org/apache/hudi/io/hadoop/TestHoodieHFileReaderWriter.java
@@ -31,9 +31,12 @@ import org.apache.hudi.common.model.HoodieRecord;
 import org.apache.hudi.common.schema.HoodieSchema;
 import org.apache.hudi.common.schema.HoodieSchemaType;
 import org.apache.hudi.common.table.HoodieTableConfig;
+import org.apache.hudi.common.table.log.NativeLogFooterMetadata;
 import org.apache.hudi.common.testutils.HoodieTestUtils;
+import org.apache.hudi.common.util.HFileUtils;
 import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.collection.ClosableIterator;
+import org.apache.hudi.exception.MetadataNotFoundException;
 import org.apache.hudi.hadoop.fs.HadoopFSUtils;
 import org.apache.hudi.io.hfile.HFileReader;
 import org.apache.hudi.io.hfile.UTF8StringKey;
@@ -201,6 +204,27 @@ public class TestHoodieHFileReaderWriter extends 
TestHoodieReaderWriterBase {
     }
   }
 
+  @Test
+  public void testReadFooterMetadata() throws Exception {
+    HoodieSchema schema = 
getSchemaFromResource(TestHoodieOrcReaderWriter.class, 
"/exampleSchemaWithMetaFields.avsc");
+    HoodieAvroHFileWriter writer = createWriter(schema, false);
+    Map<String, String> footerMetadata = new TreeMap<>();
+    footerMetadata.put(NativeLogFooterMetadata.FOOTER_METADATA_KEY, 
"{\"SCHEMA\":\"schema\"}");
+    footerMetadata.put("custom", "value");
+    writer.addFooterMetadata(footerMetadata);
+    writer.close();
+
+    HoodieStorage storage = HoodieTestUtils.getStorage(getFilePath());
+    Map<String, String> footer = new HFileUtils().readFooter(
+        storage, false, getFilePath(), 
NativeLogFooterMetadata.FOOTER_METADATA_KEY, "custom", "missing");
+
+    assertEquals(2, footer.size());
+    assertEquals("{\"SCHEMA\":\"schema\"}", 
footer.get(NativeLogFooterMetadata.FOOTER_METADATA_KEY));
+    assertEquals("value", footer.get("custom"));
+    assertThrows(MetadataNotFoundException.class,
+        () -> new HFileUtils().readFooter(storage, true, getFilePath(), 
"missing"));
+  }
+
   @ParameterizedTest
   @MethodSource("populateMetaFieldsAndTestAvroWithMeta")
   public void testWriteReadHFileWithMetaFields(boolean populateMetaFields, 
boolean testAvroWithMeta) throws Exception {

Reply via email to