wombatu-kun commented on code in PR #19247:
URL: https://github.com/apache/hudi/pull/19247#discussion_r3564196340


##########
hudi-common/src/main/java/org/apache/hudi/common/util/FileFormatUtils.java:
##########
@@ -231,6 +231,18 @@ public String[] readMinMaxRecordKeys(HoodieStorage 
storage, StoragePath filePath
   public abstract Map<String, String> readFooter(HoodieStorage storage, 
boolean required, StoragePath filePath,
                                                  String... footerNames);
 
+  /**
+   * Read data file footer entries whose keys start with the given prefix.
+   *
+   * @param storage      {@link HoodieStorage} instance
+   * @param required     require at least one matching footer entry
+   * @param filePath     data file path
+   * @param footerPrefix prefix of the footer entries to read
+   * @return matching footer entries keyed by their full names
+   */
+  public abstract Map<String, String> readFooterWithPrefix(

Review Comment:
   `HeaderMetadataType` is a closed enum and `fromFooterMetadata` drops any key 
whose suffix is not one of its values, so a prefix scan cannot surface anything 
an explicit key list would miss - including in the forward-compat case, where 
an old reader discards a new writer's unknown type under either design. Both 
call sites could pass an enum-derived key array to the existing varargs 
`readFooter`, which drops this abstract method, its four implementations, and 
the two new public methods on `HFileReader` / `HFileInfo` in `hudi-io`.
   
   If the prefix API is kept deliberately, consider adding one `protected 
abstract Map<String, String> readAllFooterEntries(...)` and implementing 
`readFooterWithPrefix` concretely here, so the filter-then-`required`-check is 
written once rather than copy-pasted across the four impls with four different 
message strings.



##########
hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/LanceUtils.java:
##########
@@ -166,6 +166,37 @@ public Map<String, String> readFooter(HoodieStorage 
storage, boolean required, S
     }
   }
 
+  @Override
+  public Map<String, String> readFooterWithPrefix(

Review Comment:
   This impl has no coverage. 
`TestHoodieSparkLanceWriter#testFooterMetadataIsWrittenToLanceSchemaMetadata` 
already asserts `readFooter` for Lance both ways, and the PR extended the HFile 
and ORC footer tests with the matching `readFooterWithPrefix` assertions, but 
Lance was skipped. No native-log e2e test reaches Lance either, since 
`HoodieFileSliceTestUtils.createNativeLogWriter` only branches HFILE/Parquet - 
so nothing exercises the allocator or the `catch (MetadataNotFoundException) { 
throw e; }` arm that keeps the trailing `catch (Exception)` from re-wrapping it.
   
   Adding the same two assertions next to the existing `readFooter` ones in 
that test would close it.



##########
hudi-hadoop-common/src/test/java/org/apache/hudi/io/hadoop/TestHoodieHFileReaderWriter.java:
##########
@@ -208,19 +209,25 @@ protected void verifySchema(HoodieStorage storage, String 
schemaPath) throws IOE
   public void testReadFooterMetadata() throws Exception {
     HoodieSchema schema = 
getSchemaFromResource(TestHoodieOrcReaderWriter.class, 
"/exampleSchemaWithMetaFields.avsc");
     HoodieAvroHFileWriter writer = createWriter(schema, false);
+    String schemaMetadataKey = 
NativeLogFooterMetadata.getFooterMetadataKey(HeaderMetadataType.SCHEMA);
     Map<String, String> footerMetadata = new TreeMap<>();
-    footerMetadata.put(NativeLogFooterMetadata.FOOTER_METADATA_KEY, 
"{\"SCHEMA\":\"schema\"}");
+    footerMetadata.put(schemaMetadataKey, "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");
+        storage, false, getFilePath(), schemaMetadataKey, "custom", "missing");
 
     assertEquals(2, footer.size());
-    assertEquals("{\"SCHEMA\":\"schema\"}", 
footer.get(NativeLogFooterMetadata.FOOTER_METADATA_KEY));
+    assertEquals("schema", footer.get(schemaMetadataKey));
     assertEquals("value", footer.get("custom"));
+
+    Map<String, String> footerByPrefix = new HFileUtils().readFooterWithPrefix(
+        storage, true, getFilePath(), 
NativeLogFooterMetadata.FOOTER_METADATA_KEY_PREFIX);

Review Comment:
   `required=true` is passed here with a matching entry present, so the new 
`MetadataNotFoundException` branch never executes - and the same holds for the 
ORC assertion and for the Parquet and Lance impls, leaving that branch untested 
across all four. The `assertThrows` below covers the old `readFooter`, not this 
method.
   
   One `assertThrows(MetadataNotFoundException.class, () -> new 
HFileUtils().readFooterWithPrefix(storage, true, getFilePath(), 
"no.such.prefix."))` here, and the equivalent in the ORC test, would pin it.



##########
hudi-common/src/main/java/org/apache/hudi/common/util/HFileUtils.java:
##########
@@ -128,6 +128,31 @@ public Map<String, String> readFooter(HoodieStorage 
storage, boolean required, S
     }
   }
 
+  @Override
+  public Map<String, String> readFooterWithPrefix(
+      HoodieStorage storage, boolean required, StoragePath filePath, String 
footerPrefix) {
+    Map<String, String> footerVals = new HashMap<>();
+    try (HFileReader reader = HFileReaderFactory.builder()
+        .withStorage(storage)
+        .withPath(filePath)
+        .build()
+        .createHFileReader()) {
+      reader.getMetaInfo().forEach((key, value) -> {
+        String footerName = fromUTF8Bytes(key.getBytes());

Review Comment:
   `Key.getBytes()` returns the backing array and ignores `offset`/`length`. 
This is correct today only because every `UTF8StringKey` happens to be built 
over an exact-size array (`HFileFileInfoBlock` constructs them from 
`ByteString.toByteArray()`), and it breaks silently the moment one is 
constructed over a shared buffer. `key.getContentInString()` is the offset-safe 
accessor, and is what `HoodieNativeAvroHFileReader` and 
`HFileBootstrapIndexReader` use.



##########
hudi-io/src/main/java/org/apache/hudi/io/hfile/HFileReader.java:
##########
@@ -78,6 +79,14 @@ public interface HFileReader extends Closeable {
    */
   Option<byte[]> getMetaInfo(UTF8StringKey key) throws IOException;
 
+  /**
+   * Gets all entries from the file info block.
+   *
+   * @return all file info entries
+   * @throws IOException upon read errors
+   */
+  Map<UTF8StringKey, byte[]> getMetaInfo() throws IOException;

Review Comment:
   Overloading `getMetaInfo` by arity gives the two methods unrelated return 
types (`Option<byte[]>` for a single key, the whole map here), so 
`reader.getMetaInfo()` reads at a call site as if it returned one value. 
`getAllMetaInfo()` would be unambiguous.
   
   The javadoc should also state that the returned map is an unmodifiable view, 
since `HFileInfo.getInfoMap()` wraps it in `Collections.unmodifiableMap` and 
callers cannot otherwise tell.



##########
rfc/rfc-103/rfc-103.md:
##########
@@ -223,19 +223,21 @@ Footer
   - key-value metadata <-- Hudi log format metadata goes in here
 ```
 
-Hudi log format metadata can be stored as a single entry in the Parquet footer 
with key = `hudi.log.format.metadata` and value being a serialized JSON of the 
metadata entries:
-
-| Hudi log format metadata                     |
-|:---------------------------------------------|
-| `VERSION`                                    |
-| `INSTANT_TIME`                               |
-| `TARGET_INSTANT_TIME`                        |
-| `COMMAND_BLOCK_TYPE`                         |
-| `COMPACTED_BLOCK_TIMES`                      |
-| `RECORD_POSITIONS`                           |
-| `BLOCK_IDENTIFIER`                           |
-| `IS_PARTIAL`                                 |
-| `BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS` |
+Each Hudi log format metadata value is stored as a separate Parquet footer 
entry under the

Review Comment:
   This restores the exact per-key mapping table that was removed during the 
RFC-103 review. In 
https://github.com/apache/hudi/pull/17827#discussion_r2830722993 @vinothchandar 
marked it IMPORTANT ("Can't we just store this in a single entry as a 
serialized map... Anyways, parquet readers will read all footer entries at 
once. We don't get any benefit by splitting them out?") and @xushiyan agreed to 
make it a single entry, which is how the RFC merged and how #19067 / #19072 
implemented it two weeks ago.
   
   Issue #19246 concedes the I/O point ("Parquet already reads the complete 
footer, so this is mainly a cleaner contract"), so the case rests on the 
tooling/escaping arguments, which were not on the table then. Worth linking 
that thread in the description and getting an ack from @vinothchandar and 
@xushiyan before flipping the on-disk contract back, since they are RFC-103's 
approver and proposer.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to