@@ -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} ;
6667use 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) ]
616641mod 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