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 5cd27ec3 perf(core): write the base file bloom filter only when asked 
(#933)
5cd27ec3 is described below

commit 5cd27ec350da5f8390a40059d35b2c9ba33c5975
Author: Y Ethan Guo <[email protected]>
AuthorDate: Fri Oct 30 11:38:20 2026 -0700

    perf(core): write the base file bloom filter only when asked (#933)
---
 crates/core/src/write/append.rs         | 17 ++++++++++++-----
 crates/core/tests/spark_parity_tests.rs | 20 ++++++++++++++++++--
 crates/core/tests/table_write_tests.rs  | 30 +++++++++++++++++++++++++++---
 scripts/parity/README.md                |  4 +++-
 4 files changed, 60 insertions(+), 11 deletions(-)

diff --git a/crates/core/src/write/append.rs b/crates/core/src/write/append.rs
index c3831d1b..e427a53b 100644
--- a/crates/core/src/write/append.rs
+++ b/crates/core/src/write/append.rs
@@ -736,8 +736,11 @@ pub(crate) fn parquet_writer_props(table: &Table) -> 
parquet::file::properties::
         .build()
 }
 
-/// The record-key bloom filter a base file footer carries, resolved as Java's
-/// `HoodieFileWriterFactory#enableBloomFilter` and `#createBloomFilter` do.
+/// The record-key bloom filter a base file footer carries, sized as Java's
+/// `HoodieFileWriterFactory#createBloomFilter` does. Off unless
+/// `hoodie.parquet.bloom.filter.enabled` is true or the index type is a bloom
+/// index: the record index locates keys without it, and hashing every key
+/// costs a large share of a bulk load.
 pub(crate) fn base_file_bloom_filter_config(table: &Table) -> 
Option<BloomFilterConfig> {
     let populates_meta_fields: bool = table
         .hudi_configs
@@ -746,7 +749,7 @@ pub(crate) fn base_file_bloom_filter_config(table: &Table) 
-> Option<BloomFilter
     let options = table.hudi_configs.as_options();
     let enabled = options
         .get("hoodie.parquet.bloom.filter.enabled")
-        .is_none_or(|v| v.eq_ignore_ascii_case("true"))
+        .is_some_and(|v| v.eq_ignore_ascii_case("true"))
         || options
             .get("hoodie.index.type")
             .is_some_and(|v| v.contains("BLOOM"));
@@ -1424,7 +1427,8 @@ mod tests {
         assert_eq!(rows, 64);
     }
 
-    /// The base file bloom filter follows Java's enable rule and sizing 
configs.
+    /// The base file bloom filter is off by default, on when asked for or when
+    /// the index type needs it, and sized by Java's configs.
     #[tokio::test]
     async fn test_base_file_bloom_filter_config_resolution() {
         async fn config(options: &[(&str, &str)]) -> Option<BloomFilterConfig> 
{
@@ -1437,7 +1441,9 @@ mod tests {
             }
             base_file_bloom_filter_config(&builder.create().await.unwrap())
         }
-        assert_eq!(config(&[]).await, Some(BloomFilterConfig::default()));
+        assert_eq!(config(&[]).await, None);
+        let enabled = ("hoodie.parquet.bloom.filter.enabled", "true");
+        assert_eq!(config(&[enabled]).await, 
Some(BloomFilterConfig::default()));
         let disabled = ("hoodie.parquet.bloom.filter.enabled", "false");
         assert_eq!(config(&[disabled]).await, None);
         assert_eq!(
@@ -1446,6 +1452,7 @@ mod tests {
         );
         assert_eq!(
             config(&[
+                enabled,
                 ("hoodie.bloom.index.filter.type", "simple"),
                 ("hoodie.index.bloom.num_entries", "100"),
                 ("hoodie.index.bloom.fpp", "0.01"),
diff --git a/crates/core/tests/spark_parity_tests.rs 
b/crates/core/tests/spark_parity_tests.rs
index 59281d24..b60fb568 100644
--- a/crates/core/tests/spark_parity_tests.rs
+++ b/crates/core/tests/spark_parity_tests.rs
@@ -276,6 +276,15 @@ fn batch(rows: &[Row]) -> RecordBatch {
 }
 
 async fn rs_create(path: &Path, table_type: TableTypeValue, partitioned: bool) 
-> Table {
+    rs_create_with(path, table_type, partitioned, &[]).await
+}
+
+async fn rs_create_with(
+    path: &Path,
+    table_type: TableTypeValue,
+    partitioned: bool,
+    options: &[(&str, &str)],
+) -> Table {
     let mut builder = Table::create(path.to_str().unwrap())
         .with_table_name("parity")
         .with_table_type(table_type)
@@ -284,9 +293,16 @@ async fn rs_create(path: &Path, table_type: 
TableTypeValue, partitioned: bool) -
     if partitioned {
         builder = builder.with_partition_fields(["part"]);
     }
+    for (key, value) in options {
+        builder = builder.with_option(key, *value);
+    }
     builder.create().await.unwrap()
 }
 
+/// Java's bloom index reads the record-key bloom filter from each base file
+/// footer, which rs writes only when asked to.
+const BLOOM_FOOTER: (&str, &str) = ("hoodie.parquet.bloom.filter.enabled", 
"true");
+
 async fn rs_read(path: &Path) -> Vec<Row> {
     rs_read_with(path, ReadOptions::new()).await
 }
@@ -1096,7 +1112,7 @@ async fn parity_i_java_index_lookups_on_rs_tables() {
         for index in ["SIMPLE", "BLOOM", "GLOBAL_BLOOM"] {
             let dir = tempdir().unwrap();
             let path = dir.path().join("t");
-            let mut table = rs_create(&path, table_type.clone(), true).await;
+            let mut table = rs_create_with(&path, table_type.clone(), true, 
&[BLOOM_FOOTER]).await;
             table
                 .append([batch(&[
                     row("a", Some("sf"), 1, 10),
@@ -1167,7 +1183,7 @@ async fn parity_i_java_index_lookups_on_rs_tables() {
     // packing off, a missed lookup writes a duplicate instead of merging.
     let dir = tempdir().unwrap();
     let path = dir.path().join("t");
-    let mut table = rs_create(&path, TableTypeValue::CopyOnWrite, true).await;
+    let mut table = rs_create_with(&path, TableTypeValue::CopyOnWrite, true, 
&[BLOOM_FOOTER]).await;
     table
         .append([batch(&[
             row("\u{FF21}", Some("sf"), 1, 10),
diff --git a/crates/core/tests/table_write_tests.rs 
b/crates/core/tests/table_write_tests.rs
index de3e714d..41a00d3e 100644
--- a/crates/core/tests/table_write_tests.rs
+++ b/crates/core/tests/table_write_tests.rs
@@ -4326,9 +4326,10 @@ fn base_file_footers(dir: &std::path::Path) -> 
Vec<(HashMap<String, String>, Vec
     footers
 }
 
-/// Every base file carries the footer Java's bloom index reads: the bloom
-/// filter at Java's default sizing holding every record key, its type code,
-/// and the record key range, which an empty file leaves out.
+/// With the bloom filter enabled, every base file carries the footer Java's
+/// bloom index reads: the bloom filter at Java's default sizing holding every
+/// record key, its type code, and the record key range, which an empty file
+/// leaves out.
 #[tokio::test]
 async fn test_base_file_footer_carries_bloom_filter_and_key_range() {
     for table_type in [TableTypeValue::CopyOnWrite, 
TableTypeValue::MergeOnRead] {
@@ -4338,6 +4339,7 @@ async fn 
test_base_file_footer_carries_bloom_filter_and_key_range() {
             .with_table_type(table_type.clone())
             .with_partition_fields(["city"])
             .with_record_key_fields(["id"])
+            .with_option("hoodie.parquet.bloom.filter.enabled", "true")
             .create()
             .await
             .unwrap();
@@ -4399,6 +4401,28 @@ fn assert_record_key_footer(footer: &HashMap<String, 
String>, keys: &[String]) {
     assert_eq!(footer.get("hoodie_max_record_key"), keys.iter().max());
 }
 
+/// By default a base file footer holds no bloom filter and no key range.
+#[tokio::test]
+async fn test_base_file_footer_omits_bloom_filter_by_default() {
+    let dir = tempdir().unwrap();
+    let mut table = Table::create(dir.path().to_str().unwrap())
+        .with_table_name("trips")
+        .with_record_key_fields(["id"])
+        .create()
+        .await
+        .unwrap();
+    table.append([sample_batch()]).await.unwrap();
+    let footers = base_file_footers(dir.path());
+    assert!(!footers.is_empty());
+    for (footer, _) in &footers {
+        assert!(
+            footer.keys().all(|key| key == "ARROW:schema"),
+            "{:?}",
+            footer.keys().collect::<Vec<_>>()
+        );
+    }
+}
+
 /// A table without meta fields writes no bloom filter, as Java does.
 #[tokio::test]
 async fn test_base_file_footer_omits_bloom_filter_without_meta_fields() {
diff --git a/scripts/parity/README.md b/scripts/parity/README.md
index 0c694708..69683563 100644
--- a/scripts/parity/README.md
+++ b/scripts/parity/README.md
@@ -38,7 +38,9 @@ Cross-checks hudi-rs against the Apache Hudi Spark 
writer/reader:
 - **I** — Spark upserts with SIMPLE, BLOOM and GLOBAL_BLOOM indexes on
   rs-written COW/MOR tables find every key rs wrote (base files, inserts,
   updates) and re-insert one rs deleted without duplicates; includes keys
-  whose UTF-8 and UTF-16 orders differ, against the bloom footer key range
+  whose UTF-8 and UTF-16 orders differ, against the bloom footer key range.
+  These tables set `hoodie.parquet.bloom.filter.enabled=true`: rs leaves the
+  footer out by default, and Java's bloom index needs it
 - **J** — Spark SQL UPDATE/DELETE/INSERT on rs-written COW/MOR tables
   registered in a non-default database, then rs writes on top
 

Reply via email to