Skip to content

Commit 96f28c1

Browse files
authored
Merge pull request JanKaul#379 from cedricziel/feat/parquet-bloom-column-properties
feat: honor per-column bloom-filter table properties in the Parquet writer
2 parents a34ce53 + 2e1d308 commit 96f28c1

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

iceberg-rust-spec/src/spec/table_metadata.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ pub const WRITE_OBJECT_STORAGE_ENABLED: &str = "write.object-storage.enabled";
4949
pub const WRITE_DATA_PATH: &str = "write.data.path";
5050
pub const WRITE_METADATA_METRICS_DISTINCT_COUNTS_ENABLED: &str =
5151
"write.metadata.metrics.distinct-counts.enabled";
52+
/// Per-column Parquet bloom-filter toggle: append the column name, e.g.
53+
/// `write.parquet.bloom-filter-enabled.column.label_env = "true"`.
54+
pub const WRITE_PARQUET_BLOOM_FILTER_ENABLED_COLUMN_PREFIX: &str =
55+
"write.parquet.bloom-filter-enabled.column.";
5256

5357
pub use _serde::{TableMetadataV1, TableMetadataV2, TableMetadataV3};
5458

iceberg-rust/src/arrow/write.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use iceberg_rust_spec::{
5151
spec::{manifest::DataFile, schema::Schema, values::Value},
5252
table_metadata::{
5353
self, WRITE_DATA_PATH, WRITE_METADATA_METRICS_DISTINCT_COUNTS_ENABLED,
54-
WRITE_OBJECT_STORAGE_ENABLED,
54+
WRITE_OBJECT_STORAGE_ENABLED, WRITE_PARQUET_BLOOM_FILTER_ENABLED_COLUMN_PREFIX,
5555
},
5656
util::strip_prefix,
5757
};
@@ -62,6 +62,7 @@ use parquet::{
6262
metadata::{KeyValue, ParquetMetaData},
6363
properties::WriterProperties,
6464
},
65+
schema::types::ColumnPath,
6566
};
6667
use uuid::Uuid;
6768

@@ -514,6 +515,7 @@ async fn create_arrow_writer(
514515

515516
let mut props_builder =
516517
WriterProperties::builder().set_compression(Compression::ZSTD(ZstdLevel::try_new(1)?));
518+
props_builder = apply_bloom_filter_properties(props_builder, table_properties);
517519
if estimate_distinct_count {
518520
props_builder = props_builder.set_key_value_metadata(Some(vec![KeyValue::new(
519521
ICEBERG_ESTIMATE_INT64_DISTINCT_COUNT_META_KEY.to_owned(),
@@ -531,6 +533,29 @@ async fn create_arrow_writer(
531533
))
532534
}
533535

536+
/// Applies per-column bloom-filter table properties to the writer builder.
537+
///
538+
/// Honors the standard Iceberg property
539+
/// `write.parquet.bloom-filter-enabled.column.<name>` = `true`/`false`
540+
/// per column. Unrelated properties are ignored.
541+
fn apply_bloom_filter_properties(
542+
mut props_builder: parquet::file::properties::WriterPropertiesBuilder,
543+
table_properties: &HashMap<String, String>,
544+
) -> parquet::file::properties::WriterPropertiesBuilder {
545+
for (key, value) in table_properties {
546+
if let Some(column) = key.strip_prefix(WRITE_PARQUET_BLOOM_FILTER_ENABLED_COLUMN_PREFIX) {
547+
let enabled = value.eq_ignore_ascii_case("true");
548+
// Parquet addresses nested columns by path parts; a dotted name
549+
// passed as one string would be treated as a single segment and
550+
// silently never match.
551+
let path_parts: Vec<String> = column.split('.').map(String::from).collect();
552+
props_builder = props_builder
553+
.set_column_bloom_filter_enabled(ColumnPath::from(path_parts), enabled);
554+
}
555+
}
556+
props_builder
557+
}
558+
534559
/// Generates a unique file path for a Parquet data file.
535560
///
536561
/// This function creates a unique file path by combining the data location, partition path,
@@ -614,6 +639,73 @@ fn record_batch_size(batch: &RecordBatch) -> usize {
614639

615640
#[cfg(test)]
616641
mod tests {
642+
use super::*;
643+
644+
#[test]
645+
fn bloom_filter_properties_apply_per_column() {
646+
let table_properties = HashMap::from([
647+
(
648+
"write.parquet.bloom-filter-enabled.column.label_env".to_string(),
649+
"true".to_string(),
650+
),
651+
(
652+
"write.parquet.bloom-filter-enabled.column.body".to_string(),
653+
"false".to_string(),
654+
),
655+
("write.data.path".to_string(), "s3://x".to_string()),
656+
]);
657+
let props =
658+
apply_bloom_filter_properties(WriterProperties::builder(), &table_properties).build();
659+
assert!(props
660+
.bloom_filter_properties(&ColumnPath::from("label_env"))
661+
.is_some());
662+
assert!(props
663+
.bloom_filter_properties(&ColumnPath::from("body"))
664+
.is_none());
665+
assert!(props
666+
.bloom_filter_properties(&ColumnPath::from("other"))
667+
.is_none());
668+
}
669+
670+
#[test]
671+
fn bloom_filter_properties_apply_to_nested_columns() {
672+
let table_properties = HashMap::from([(
673+
"write.parquet.bloom-filter-enabled.column.my_struct.label_env".to_string(),
674+
"true".to_string(),
675+
)]);
676+
let props =
677+
apply_bloom_filter_properties(WriterProperties::builder(), &table_properties).build();
678+
// Parquet addresses nested columns by path parts, not by the dotted string.
679+
assert!(props
680+
.bloom_filter_properties(&ColumnPath::from(vec![
681+
"my_struct".to_string(),
682+
"label_env".to_string()
683+
]))
684+
.is_some());
685+
}
686+
687+
#[test]
688+
fn bloom_filter_property_values_are_case_insensitive() {
689+
let table_properties = HashMap::from([
690+
(
691+
"write.parquet.bloom-filter-enabled.column.label_env".to_string(),
692+
"TRUE".to_string(),
693+
),
694+
(
695+
"write.parquet.bloom-filter-enabled.column.body".to_string(),
696+
"False".to_string(),
697+
),
698+
]);
699+
let props =
700+
apply_bloom_filter_properties(WriterProperties::builder(), &table_properties).build();
701+
assert!(props
702+
.bloom_filter_properties(&ColumnPath::from("label_env"))
703+
.is_some());
704+
assert!(props
705+
.bloom_filter_properties(&ColumnPath::from("body"))
706+
.is_none());
707+
}
708+
617709
use iceberg_rust_spec::{
618710
partition::BoundPartitionField,
619711
types::{StructField, Type},

0 commit comments

Comments
 (0)