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

yihua pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hudi-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new ef475f7e Read every metadata partition's log blocks, not just the 
files partition (#692)
ef475f7e is described below

commit ef475f7e44db963f9c21a4859165bd926de98c1f
Author: Lin Liu <[email protected]>
AuthorDate: Wed Sep 2 15:08:42 2026 -0700

    Read every metadata partition's log blocks, not just the files partition 
(#692)
---
 crates/core/src/file_group/base_file/hfile.rs | 226 ++++++++++++++++++++++++++
 1 file changed, 226 insertions(+)

diff --git a/crates/core/src/file_group/base_file/hfile.rs 
b/crates/core/src/file_group/base_file/hfile.rs
index 32a93462..e1e73ff0 100644
--- a/crates/core/src/file_group/base_file/hfile.rs
+++ b/crates/core/src/file_group/base_file/hfile.rs
@@ -1584,4 +1584,230 @@ mod tests {
         );
         Ok(())
     }
+
+    /// One metadata-table file slice: the partition it lives in, its base 
file if
+    /// the slice has one, and the log files written after it.
+    struct MdtSlice {
+        label: &'static str,
+        partition: &'static str,
+        base_file: Option<&'static str>,
+        log_files: &'static [&'static str],
+        /// The base instant the slice is read as of.
+        instant: &'static str,
+    }
+
+    /// Every metadata partition in the fixture that has a merge-reachable 
slice.
+    ///
+    /// `column_stats`, `partition_stats` and `record_index` compact to a base 
file
+    /// written *after* all their log files, so their pre-compaction slice has 
log
+    /// files and no base file at all. That is the sharper case for this 
change: an
+    /// HFile log block that is skipped contributes nothing, so the read has no
+    /// records and no schema to build an output from, and fails outright.
+    const MDT_SLICES: &[MdtSlice] = &[
+        MdtSlice {
+            label: "files",
+            partition: "files",
+            base_file: Some("files-0000-0_0-955-2690_00000000000000000.hfile"),
+            log_files: &[
+                ".files-0000-0_20251220210108078.log.1_10-999-2838",
+                ".files-0000-0_20251220210123755.log.1_3-1032-2950",
+                ".files-0000-0_20251220210125441.log.1_5-1057-3024",
+                ".files-0000-0_20251220210127080.log.1_3-1082-3100",
+                ".files-0000-0_20251220210128625.log.1_5-1107-3174",
+                ".files-0000-0_20251220210129235.log.1_3-1118-3220",
+                ".files-0000-0_20251220210130911.log.1_3-1149-3338",
+            ],
+            instant: "00000000000000000",
+        },
+        MdtSlice {
+            label: "column_stats",
+            partition: "column_stats",
+            base_file: None,
+            log_files: &[
+                ".col-stats-0000-0_20251220210108078.log.1_7-999-2835",
+                ".col-stats-0000-0_20251220210123755.log.1_0-1032-2947",
+                ".col-stats-0000-0_20251220210125441.log.1_2-1057-3021",
+                ".col-stats-0000-0_20251220210127080.log.1_0-1082-3097",
+                ".col-stats-0000-0_20251220210128625.log.1_2-1107-3171",
+                ".col-stats-0000-0_20251220210129235.log.1_0-1118-3217",
+                ".col-stats-0000-0_20251220210130911.log.1_0-1149-3335",
+            ],
+            instant: "00000000000000001",
+        },
+        MdtSlice {
+            label: "partition_stats",
+            partition: "partition_stats",
+            base_file: None,
+            log_files: &[
+                ".partition-stats-0000-0_20251220210108078.log.1_9-999-2837",
+                ".partition-stats-0000-0_20251220210123755.log.1_2-1032-2949",
+                ".partition-stats-0000-0_20251220210125441.log.1_4-1057-3023",
+                ".partition-stats-0000-0_20251220210127080.log.1_2-1082-3099",
+                ".partition-stats-0000-0_20251220210128625.log.1_4-1107-3173",
+                ".partition-stats-0000-0_20251220210129235.log.1_2-1118-3219",
+                ".partition-stats-0000-0_20251220210130911.log.1_2-1149-3337",
+            ],
+            instant: "00000000000000003",
+        },
+        MdtSlice {
+            label: "record_index",
+            partition: "record_index",
+            base_file: None,
+            log_files: 
&[".record-index-0001-0_20251220210108078.log.1_0-999-2828"],
+            instant: "00000000000000002",
+        },
+        MdtSlice {
+            label: "secondary_index",
+            partition: "secondary_index_rider_idx",
+            base_file: 
Some("secondary-index-rider-idx-0002-0_2-1008-2876_00000000000000004.hfile"),
+            log_files: 
&[".secondary-index-rider-idx-0002-0_20251220210125441.log.1_0-1057-3019"],
+            instant: "00000000000000004",
+        },
+    ];
+
+    /// Read one slice through the v2 reader, optionally without its log files.
+    async fn read_mdt_slice(slice: &MdtSlice, with_logs: bool) -> 
crate::Result<RecordBatch> {
+        let configs = mdt_configs();
+        let storage = Storage::new(Arc::new(HashMap::new()), configs.clone())?;
+        let logs: Vec<String> = if with_logs {
+            slice
+                .log_files
+                .iter()
+                .map(|f| format!("{}/{f}", slice.partition))
+                .collect()
+        } else {
+            vec![]
+        };
+        // The base file's path is threaded in so the format resolves per path 
rather
+        // than from config alone; the metadata table's base files are HFile.
+        let base_file_path = slice.base_file.map(|b| format!("{}/{b}", 
slice.partition));
+        let mut context =
+            resolve_reader_context(&configs, !logs.is_empty(), 
base_file_path.as_deref())?;
+        context.rebuild_record_context(slice.partition.to_string());
+        let mut reader = HoodieFileGroupReader::new(
+            Arc::new(context),
+            storage,
+            InputSplit::new(
+                base_file_path.clone(),
+                Some(slice.instant.to_string()),
+                logs,
+                slice.partition.to_string(),
+            ),
+            ReaderParameters::default(),
+            None,
+            None,
+        )?;
+        reader.read().await
+    }
+
+    /// The record keys of a metadata read.
+    fn record_keys(batch: &RecordBatch) -> HashSet<String> {
+        batch
+            .column_by_name("key")
+            .expect("the metadata record key column, decoded from the HFile's 
values")
+            .as_string::<i32>()
+            .iter()
+            .flatten()
+            .map(str::to_string)
+            .collect()
+    }
+
+    /// Every metadata partition in the fixture reads through the v2 reader 
with its
+    /// HFile log blocks contributing.
+    ///
+    /// One case per partition rather than the `files` partition alone, 
because a
+    /// skipped log block is silent: the read succeeds and returns the base 
file's
+    /// rows, so a `files`-only test would pass on four of the five partitions
+    /// regardless.
+    ///
+    /// Two shapes of assertion, chosen by what the slice can prove:
+    ///
+    /// - **No base file** (`column_stats`, `partition_stats`, 
`record_index`). Every
+    ///   record comes from a log block, so a skipped block leaves the read 
with no
+    ///   records and no schema to build an output from, and it fails. 
Returning any
+    ///   row is the assertion.
+    /// - **Base file and log files** (`files`, `secondary_index`). A skipped 
block
+    ///   makes the merged read identical to a base-file-only read, so the two 
must
+    ///   differ.
+    ///
+    /// What this pins is that the blocks are **decoded and reach the merge**, 
not
+    /// what the merge then does with them. The rule the merge applies is the
+    /// metadata payload's own, asserted separately by
+    /// `v2_folds_a_metadata_files_slice_like_the_metadata_table_reader`, which
+    /// compares the folded values rather than the key set.
+    #[tokio::test]
+    async fn v2_reads_every_metadata_partition_with_its_log_blocks() -> 
crate::Result<()> {
+        for slice in MDT_SLICES {
+            let merged = read_mdt_slice(slice, true).await.map_err(|e| {
+                StorageError::Creation(format!(
+                    "{}: reading with log files failed: {e:?}",
+                    slice.label
+                ))
+            })?;
+            let merged_keys = record_keys(&merged);
+            assert!(
+                !merged_keys.is_empty(),
+                "{}: a read whose log blocks contribute must return records",
+                slice.label
+            );
+
+            match slice.base_file {
+                None => {
+                    // Nothing but log blocks, so the rows are proof on their 
own.
+                }
+                Some(_) => {
+                    let base_only = read_mdt_slice(slice, false).await?;
+                    assert_ne!(
+                        (record_keys(&base_only), base_only.num_rows()),
+                        (merged_keys.clone(), merged.num_rows()),
+                        "{}: adding log files changed nothing, so the blocks 
were skipped",
+                        slice.label
+                    );
+                }
+            }
+        }
+        Ok(())
+    }
+
+    /// The `files` partition, checked against an oracle rather than against 
itself.
+    ///
+    /// `MetadataTableFileGroupReader` reads the same slice through a separate
+    /// implementation that decodes to Rust structs instead of Arrow, so 
agreeing
+    /// with it is evidence about the records rather than about this code path.
+    /// It is the only metadata partition that reader serves, which is why the
+    /// other four are covered by the weaker assertions above.
+    #[tokio::test]
+    async fn v2_reads_a_files_slice_matching_the_metadata_table_reader() -> 
crate::Result<()> {
+        let slice = &MDT_SLICES[0];
+        assert_eq!(slice.label, "files", "this case reads the files 
partition");
+
+        let configs = mdt_configs();
+        let storage = Storage::new(Arc::new(HashMap::new()), configs.clone())?;
+        let mut fg = FileGroup::new(
+            MDT_FILES_FILE_GROUP.to_string(),
+            MDT_FILES_PARTITION.to_string(),
+        );
+        fg.add_base_file_from_name(slice.base_file.expect("the files slice has 
a base file"))?;
+        fg.add_log_files_from_names(slice.log_files.iter().copied())?;
+        let expected: HashSet<String> = 
MetadataTableFileGroupReader::new(configs, storage)
+            .read_files_partition(
+                fg.get_file_slice_as_of(MAX_INSTANT_TIME)
+                    .expect("the file group has a slice"),
+                &[],
+            )
+            .await?
+            .into_keys()
+            .collect();
+        assert!(
+            !expected.is_empty(),
+            "the oracle must return keys, or the comparison is vacuous"
+        );
+
+        assert_eq!(
+            record_keys(&read_mdt_slice(slice, true).await?),
+            expected,
+            "the v2 read of a files slice must return the metadata-table 
reader's keys"
+        );
+        Ok(())
+    }
 }

Reply via email to