@@ -1337,11 +1337,17 @@ impl RowGroupsPrunedParquetOpen {
13371337 replace_columns_with_literals ( expr, state. null_replacements ( ) )
13381338 } ) ?,
13391339 } ;
1340+ let pushdown_predicate = prepared
1341+ . pushdown_filters
1342+ . then_some ( prepared. predicate . as_ref ( ) )
1343+ . flatten ( ) ;
13401344 let read_plan = build_projection_read_plan (
1341- projection. expr_iter ( ) ,
1345+ projection. expr_iter ( ) . chain ( pushdown_predicate . cloned ( ) ) ,
13421346 & prepared. physical_file_schema ,
13431347 parquet_metadata. file_metadata ( ) . schema_descr ( ) ,
13441348 ) ;
1349+ // Partial indexes are supported: only a column read from a row group
1350+ // with a partial selection needs an offset index to benefit.
13451351 if selected_row_groups. any ( |idx| {
13461352 parquet_metadata
13471353 . row_group ( idx)
@@ -1395,7 +1401,14 @@ impl RowGroupsPrunedParquetOpen {
13951401 . loaded
13961402 . options
13971403 . clone ( )
1398- . with_page_index_policy ( PageIndexPolicy :: Optional ) ,
1404+ . with_offset_index_policy ( PageIndexPolicy :: Optional )
1405+ . with_column_index_policy (
1406+ if self . prepared . page_pruning_predicate . is_some ( ) {
1407+ PageIndexPolicy :: Optional
1408+ } else {
1409+ PageIndexPolicy :: Skip
1410+ } ,
1411+ ) ,
13991412 )
14001413 . await ?;
14011414
@@ -2058,12 +2071,14 @@ async fn load_page_index<T: AsyncFileReader>(
20582071 options : ArrowReaderOptions ,
20592072) -> Result < ArrowReaderMetadata > {
20602073 let parquet_metadata = reader_metadata. metadata ( ) ;
2061- let missing_column_index = !parquet_metadata
2062- . page_index ( )
2063- . is_some_and ( |page_index| page_index. has_column_indexes ( ) ) ;
2064- let missing_offset_index = !parquet_metadata
2065- . page_index ( )
2066- . is_some_and ( |page_index| page_index. has_offset_indexes ( ) ) ;
2074+ let missing_column_index = options. column_index_policy ( ) != PageIndexPolicy :: Skip
2075+ && !parquet_metadata
2076+ . page_index ( )
2077+ . is_some_and ( |page_index| page_index. has_column_indexes ( ) ) ;
2078+ let missing_offset_index = options. offset_index_policy ( ) != PageIndexPolicy :: Skip
2079+ && !parquet_metadata
2080+ . page_index ( )
2081+ . is_some_and ( |page_index| page_index. has_offset_indexes ( ) ) ;
20672082 // You may ask yourself: why are we even checking if the page index is already loaded here?
20682083 // Didn't we explicitly *not* load it above?
20692084 // Well it's possible that a custom implementation of `AsyncFileReader` gives you
@@ -2073,7 +2088,8 @@ async fn load_page_index<T: AsyncFileReader>(
20732088 let m = Arc :: try_unwrap ( Arc :: clone ( parquet_metadata) )
20742089 . unwrap_or_else ( |e| e. as_ref ( ) . clone ( ) ) ;
20752090 let mut reader = ParquetMetaDataReader :: new_with_metadata ( m)
2076- . with_page_index_policy ( PageIndexPolicy :: Optional ) ;
2091+ . with_column_index_policy ( options. column_index_policy ( ) )
2092+ . with_offset_index_policy ( options. offset_index_policy ( ) ) ;
20772093 reader. load_page_index ( input) . await ?;
20782094 let new_parquet_metadata = reader. finish ( ) ?;
20792095 let new_arrow_reader =
@@ -4650,6 +4666,43 @@ mod test {
46504666 expected,
46514667 ) ;
46524668 }
4669+ for projection in [ vec ! [ 0 ] , vec ! [ 1 ] , vec ! [ 0 , 1 ] , vec ! [ ] ] {
4670+ assert_eq ! (
4671+ should_load_page_index_with_projection(
4672+ page_index_metadata( & [ ( "a" , true ) , ( "b" , true ) ] , 1 ) ,
4673+ None ,
4674+ plan. clone( ) ,
4675+ Some ( & projection) ,
4676+ ) ,
4677+ !projection. is_empty( ) ,
4678+ ) ;
4679+ }
4680+ // A missing index in a skipped row group does not prevent loading.
4681+ let mixed_metadata = page_index_metadata ( & [ ( "a" , true ) ] , 1 )
4682+ . into_builder ( )
4683+ . set_row_groups ( vec ! [
4684+ page_index_metadata( & [ ( "a" , true ) ] , 1 ) . row_group( 0 ) . clone( ) ,
4685+ page_index_metadata( & [ ( "a" , false ) ] , 1 ) . row_group( 0 ) . clone( ) ,
4686+ ] )
4687+ . build ( ) ;
4688+ let mixed_plan =
4689+ ParquetAccessPlan :: new ( vec ! [ plan. inner( ) [ 0 ] . clone( ) , RowGroupAccess :: Skip ] ) ;
4690+ assert ! ( should_load_page_index(
4691+ mixed_metadata. clone( ) ,
4692+ None ,
4693+ mixed_plan
4694+ ) ) ;
4695+ // An index in a skipped or fully scanned row group does not help the
4696+ // partially selected row group that lacks an index.
4697+ for access in [ RowGroupAccess :: Skip , RowGroupAccess :: Scan ] {
4698+ let mixed_plan =
4699+ ParquetAccessPlan :: new ( vec ! [ access, plan. inner( ) [ 0 ] . clone( ) ] ) ;
4700+ assert ! ( !should_load_page_index(
4701+ mixed_metadata. clone( ) ,
4702+ None ,
4703+ mixed_plan
4704+ ) ) ;
4705+ }
46534706 // Projection restriction applies only to the external-selection path.
46544707 assert ! ( should_load_page_index_with_projection(
46554708 metadata,
@@ -4659,6 +4712,77 @@ mod test {
46594712 ) ) ;
46604713 }
46614714
4715+ #[ tokio:: test]
4716+ async fn test_load_page_index_policies_and_cached_offset_index ( ) {
4717+ let store: Arc < dyn ObjectStore > = Arc :: new ( InMemory :: new ( ) ) ;
4718+ let batch = record_batch ! ( ( "a" , Int32 , [ 1 , 2 , 3 ] ) ) . unwrap ( ) ;
4719+ let schema = batch. schema ( ) ;
4720+ let props = WriterProperties :: builder ( )
4721+ . set_statistics_enabled ( EnabledStatistics :: Page )
4722+ . build ( ) ;
4723+ let data_len = write_parquet_batches (
4724+ Arc :: clone ( & store) ,
4725+ "test.parquet" ,
4726+ vec ! [ batch] ,
4727+ Some ( props) ,
4728+ )
4729+ . await ;
4730+ let metrics = ExecutionPlanMetricsSet :: new ( ) ;
4731+ let opener = ParquetMorselizerBuilder :: new ( )
4732+ . with_store ( store)
4733+ . with_schema ( schema)
4734+ . with_metrics ( metrics. clone ( ) )
4735+ . build ( ) ;
4736+ let file = PartitionedFile :: new ( "test.parquet" . to_string ( ) , data_len as u64 ) ;
4737+ let mut loaded = opener
4738+ . prepare_open_file ( file)
4739+ . unwrap ( )
4740+ . load ( )
4741+ . await
4742+ . unwrap ( ) ;
4743+ let options = loaded
4744+ . options
4745+ . clone ( )
4746+ . with_column_index_policy ( PageIndexPolicy :: Skip )
4747+ . with_offset_index_policy ( PageIndexPolicy :: Optional ) ;
4748+ let metadata = load_page_index (
4749+ loaded. reader_metadata ,
4750+ & mut loaded. prepared . async_file_reader ,
4751+ options. clone ( ) ,
4752+ )
4753+ . await
4754+ . unwrap ( ) ;
4755+ let page_index = metadata. metadata ( ) . page_index ( ) . unwrap ( ) ;
4756+ assert ! ( page_index. has_offset_indexes( ) ) ;
4757+ assert ! ( !page_index. has_column_indexes( ) ) ;
4758+ let bytes_scanned = counter_metric_value ( & metrics, "bytes_scanned" ) ;
4759+ let cached = load_page_index (
4760+ metadata. clone ( ) ,
4761+ & mut loaded. prepared . async_file_reader ,
4762+ options. clone ( ) ,
4763+ )
4764+ . await
4765+ . unwrap ( ) ;
4766+ assert ! ( Arc :: ptr_eq( metadata. metadata( ) , cached. metadata( ) ) ) ;
4767+ assert_eq ! (
4768+ counter_metric_value( & metrics, "bytes_scanned" ) ,
4769+ bytes_scanned
4770+ ) ;
4771+
4772+ // A later predicate scan must still load the missing column index.
4773+ let metadata = load_page_index (
4774+ cached,
4775+ & mut loaded. prepared . async_file_reader ,
4776+ options. with_column_index_policy ( PageIndexPolicy :: Optional ) ,
4777+ )
4778+ . await
4779+ . unwrap ( ) ;
4780+ let page_index = metadata. metadata ( ) . page_index ( ) . unwrap ( ) ;
4781+ assert ! ( page_index. has_offset_indexes( ) ) ;
4782+ assert ! ( page_index. has_column_indexes( ) ) ;
4783+ assert ! ( counter_metric_value( & metrics, "bytes_scanned" ) > bytes_scanned) ;
4784+ }
4785+
46624786 #[ tokio:: test]
46634787 async fn test_page_index_with_external_row_selection ( ) {
46644788 use parquet:: arrow:: arrow_reader:: { RowSelection , RowSelector } ;
@@ -4723,6 +4847,33 @@ mod test {
47234847 "offset indexes should reduce I/O: {bytes_scanned:?}"
47244848 ) ;
47254849 }
4850+ // Filter columns still read pages when the output projection is empty.
4851+ let mut bytes_scanned = Vec :: new ( ) ;
4852+ for enabled in [ false , true ] {
4853+ let metrics = ExecutionPlanMetricsSet :: new ( ) ;
4854+ let opener = ParquetMorselizerBuilder :: new ( )
4855+ . with_store ( Arc :: clone ( & store) )
4856+ . with_schema ( Arc :: clone ( & schema) )
4857+ . with_projection_indices ( & [ ] )
4858+ . with_pushdown_filters ( true )
4859+ . with_enable_page_index ( enabled)
4860+ . with_predicate ( logical2physical ( & col ( "a" ) . gt_eq ( lit ( 9950i32 ) ) , & schema) )
4861+ . with_metrics ( metrics. clone ( ) )
4862+ . build ( ) ;
4863+ let batches = open_file ( & opener, file. clone ( ) )
4864+ . await
4865+ . unwrap ( )
4866+ . try_collect :: < Vec < _ > > ( )
4867+ . await
4868+ . unwrap ( ) ;
4869+ assert_eq ! (
4870+ batches. iter( ) . map( |batch| batch. num_rows( ) ) . sum:: <usize >( ) ,
4871+ 50
4872+ ) ;
4873+ assert ! ( batches. iter( ) . all( |batch| batch. num_columns( ) == 0 ) ) ;
4874+ bytes_scanned. push ( counter_metric_value ( & metrics, "bytes_scanned" ) ) ;
4875+ }
4876+ assert ! ( bytes_scanned[ 1 ] < bytes_scanned[ 0 ] , "{bytes_scanned:?}" ) ;
47264877 }
47274878
47284879 #[ test]
0 commit comments