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

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new ede9ceb7 [rust] Add parquet.filter.columnindex.enabled option to 
control Page Index pruning (#875)
ede9ceb7 is described below

commit ede9ceb7af797f09f8d57b2a96efb8dc23040443
Author: XiaoHongbo <[email protected]>
AuthorDate: Sun Sep 20 10:22:10 2026 +0800

    [rust] Add parquet.filter.columnindex.enabled option to control Page Index 
pruning (#875)
---
 crates/paimon/src/arrow/format/mod.rs              |  13 +-
 crates/paimon/src/arrow/format/parquet.rs          | 179 +++++++++++++++++++--
 crates/paimon/src/spec/core_options.rs             |  49 ++++++
 crates/paimon/src/spec/schema.rs                   |  21 +++
 crates/paimon/src/table/audit_log_table/read.rs    |   1 +
 crates/paimon/src/table/data_evolution_reader.rs   |  16 ++
 crates/paimon/src/table/data_file_reader.rs        |  17 +-
 crates/paimon/src/table/format_table_read.rs       |   2 +
 .../paimon/src/table/full_text_search_builder.rs   |   3 +-
 crates/paimon/src/table/hybrid_search_builder.rs   |   6 +-
 crates/paimon/src/table/kv_file_reader.rs          |   1 +
 crates/paimon/src/table/pk_vector_read.rs          |   9 +-
 crates/paimon/src/table/table_read.rs              |   3 +
 crates/paimon/src/table/vector_search_result.rs    |   6 +-
 docs/src/getting-started.md                        |   7 +
 15 files changed, 301 insertions(+), 32 deletions(-)

diff --git a/crates/paimon/src/arrow/format/mod.rs 
b/crates/paimon/src/arrow/format/mod.rs
index 4fc9f93b..4cd42a3c 100644
--- a/crates/paimon/src/arrow/format/mod.rs
+++ b/crates/paimon/src/arrow/format/mod.rs
@@ -195,28 +195,29 @@ pub(crate) fn create_format_reader(
         path,
         blob_as_descriptor,
         read_fields,
+        &HashMap::new(),
         None,
         blob::DEFAULT_BLOB_READ_PARALLELISM,
         MosaicPrefetchOptions::default(),
     )
 }
 
-/// Create a format reader with a scan-shared Parquet resource budget and the
-/// per-file Mosaic prefetch settings.
+/// Create a format reader with table options and runtime read resources.
 pub(crate) fn create_format_reader_with_budget(
     path: &str,
     blob_as_descriptor: bool,
     read_fields: &[DataField],
+    table_options: &HashMap<String, String>,
     parquet_read_budget: Option<Arc<ReadBudget>>,
     blob_parallelism: usize,
     mosaic_prefetch: MosaicPrefetchOptions,
 ) -> crate::Result<Box<dyn FormatFileReader>> {
     let lower = path.to_ascii_lowercase();
     let reader: Box<dyn FormatFileReader> = if lower.ends_with(".parquet") {
-        Box::new(match parquet_read_budget {
-            Some(read_budget) => 
parquet::ParquetFormatReader::with_read_budget(read_budget),
-            None => parquet::ParquetFormatReader::default(),
-        })
+        Box::new(parquet::ParquetFormatReader::with_options(
+            table_options,
+            parquet_read_budget,
+        )?)
     } else if lower.ends_with(".blob") {
         Box::new(
             blob::BlobFormatReader::new(path.to_string(), blob_as_descriptor)
diff --git a/crates/paimon/src/arrow/format/parquet.rs 
b/crates/paimon/src/arrow/format/parquet.rs
index 758fb36e..d4ddd584 100644
--- a/crates/paimon/src/arrow/format/parquet.rs
+++ b/crates/paimon/src/arrow/format/parquet.rs
@@ -54,17 +54,45 @@ use std::ops::Range;
 use std::sync::Arc;
 use tokio::sync::mpsc;
 
-#[derive(Default)]
 pub(crate) struct ParquetFormatReader {
     read_budget: Option<Arc<ReadBudget>>,
+    page_index_enabled: bool,
+}
+
+impl Default for ParquetFormatReader {
+    fn default() -> Self {
+        Self {
+            read_budget: None,
+            page_index_enabled: true,
+        }
+    }
 }
 
 impl ParquetFormatReader {
-    pub(crate) fn with_read_budget(read_budget: Arc<ReadBudget>) -> Self {
+    pub(crate) fn with_options(
+        options: &HashMap<String, String>,
+        read_budget: Option<Arc<ReadBudget>>,
+    ) -> crate::Result<Self> {
+        Ok(Self {
+            read_budget,
+            page_index_enabled: crate::spec::CoreOptions::new(options)
+                .parquet_filter_column_index_enabled()?,
+        })
+    }
+
+    #[cfg(test)]
+    fn with_read_budget(read_budget: Arc<ReadBudget>) -> Self {
         Self {
             read_budget: Some(read_budget),
+            ..Default::default()
         }
     }
+
+    #[cfg(test)]
+    fn with_page_index_enabled(mut self, enabled: bool) -> Self {
+        self.page_index_enabled = enabled;
+        self
+    }
 }
 
 enum ParquetRowGroupMessage {
@@ -375,11 +403,13 @@ impl FormatFileReader for ParquetFormatReader {
         // Predicates need both indexes for page-stat pruning. Row selection 
only
         // needs OffsetIndex so arrow-rs can avoid fetching unselected pages.
         let mut arrow_options = ArrowReaderOptions::new();
-        if !preds.is_empty() {
-            arrow_options = 
arrow_options.with_column_index_policy(PageIndexPolicy::Optional);
-        }
-        if !preds.is_empty() || row_selection.is_some() {
-            arrow_options = 
arrow_options.with_offset_index_policy(PageIndexPolicy::Optional);
+        if self.page_index_enabled {
+            if !preds.is_empty() {
+                arrow_options = 
arrow_options.with_column_index_policy(PageIndexPolicy::Optional);
+            }
+            if !preds.is_empty() || row_selection.is_some() {
+                arrow_options = 
arrow_options.with_offset_index_policy(PageIndexPolicy::Optional);
+            }
         }
         let mut batch_stream_builder =
             
ParquetRecordBatchStreamBuilder::new_with_options(arrow_file_reader, 
arrow_options)
@@ -497,8 +527,11 @@ impl FormatFileReader for ParquetFormatReader {
         // Page-level selection. Returns `None` when ColumnIndex / OffsetIndex 
are
         // absent (page index not loaded, older files, writer without page 
index)
         // or when no page could be skipped, so intersecting is a no-op then.
-        let page_selection =
-            build_predicate_page_selection(batch_stream_builder.metadata(), 
preds, file_fields)?;
+        let page_selection = if self.page_index_enabled {
+            build_predicate_page_selection(batch_stream_builder.metadata(), 
preds, file_fields)?
+        } else {
+            None
+        };
         combined_selection = 
intersect_optional_row_selections(combined_selection, page_selection);
 
         if let Some(ref ranges) = row_selection {
@@ -3552,6 +3585,99 @@ mod tests {
         buf
     }
 
+    async fn write_page_pruning_io_parquet() -> Vec<u8> {
+        const ROWS: i32 = 1024;
+        let schema = Arc::new(ArrowSchema::new(vec![
+            ArrowField::new("id", ArrowDataType::Int32, false),
+            ArrowField::new("payload", ArrowDataType::Utf8, false),
+        ]));
+        let payloads = (0..ROWS)
+            .map(|row| format!("{row:04}-{}", "x".repeat(4096)))
+            .collect::<Vec<_>>();
+        let batch = RecordBatch::try_new(
+            Arc::clone(&schema),
+            vec![
+                Arc::new(Int32Array::from_iter_values(0..ROWS)),
+                Arc::new(StringArray::from(payloads)),
+            ],
+        )
+        .unwrap();
+        let props = parquet::file::properties::WriterProperties::builder()
+            .set_compression(Compression::UNCOMPRESSED)
+            .set_dictionary_enabled(false)
+            .set_data_page_row_count_limit(64)
+            .set_write_batch_size(64)
+            .set_max_row_group_row_count(Some(ROWS as usize))
+            .build();
+        let mut buf = Vec::new();
+        let mut writer = AsyncArrowWriter::try_new(&mut buf, schema, 
Some(props)).unwrap();
+        writer.write(&batch).await.unwrap();
+        writer.close().await.unwrap();
+        buf
+    }
+
+    async fn read_page_pruning_io(data: Bytes, page_index_enabled: bool) -> 
(Vec<String>, u64) {
+        let file_size = data.len() as u64;
+        let file_read = TrackingFileRead::new(data);
+        let tracker = file_read.clone();
+        let fields = vec![
+            int_field("id"),
+            DataField::new(
+                1,
+                "payload".to_string(),
+                DataType::VarChar(VarCharType::string_type()),
+            ),
+        ];
+        let predicates = FilePredicates {
+            predicates: vec![id_leaf(PredicateOperator::Eq, 
vec![Datum::Int(1)])],
+            row_filter_factory: None,
+            file_fields: fields.clone(),
+        };
+        let stream = ParquetFormatReader::default()
+            .with_page_index_enabled(page_index_enabled)
+            .read_batch_stream(
+                Box::new(file_read),
+                file_size,
+                &fields[1..],
+                Some(&predicates),
+                Some(64),
+                None,
+            )
+            .await
+            .unwrap();
+        tracker.reset();
+        let batches = stream.try_collect::<Vec<_>>().await.unwrap();
+        let payloads = batches
+            .iter()
+            .flat_map(|batch| {
+                let values = batch
+                    .column_by_name("payload")
+                    .unwrap()
+                    .as_any()
+                    .downcast_ref::<StringArray>()
+                    .unwrap();
+                (0..values.len())
+                    .map(|index| values.value(index).to_string())
+                    .collect::<Vec<_>>()
+            })
+            .collect();
+        (payloads, tracker.bytes_read())
+    }
+
+    #[tokio::test]
+    async fn test_predicate_page_pruning_option_controls_io_not_results() {
+        let data = Bytes::from(write_page_pruning_io_parquet().await);
+        let (enabled_rows, enabled_bytes) = read_page_pruning_io(data.clone(), 
true).await;
+        let (disabled_rows, disabled_bytes) = read_page_pruning_io(data, 
false).await;
+
+        assert_eq!(enabled_rows, disabled_rows);
+        assert_eq!(enabled_rows.len(), 1);
+        assert!(
+            enabled_bytes * 2 < disabled_bytes,
+            "page-index read used {enabled_bytes} bytes; disabled read used 
{disabled_bytes} bytes"
+        );
+    }
+
     #[derive(Clone)]
     struct TrackingFileRead {
         data: Bytes,
@@ -3625,7 +3751,11 @@ mod tests {
         buf
     }
 
-    async fn read_nested_rows(data: Bytes, row_selection: 
Option<Vec<RowRange>>) -> (usize, u64) {
+    async fn read_nested_rows(
+        data: Bytes,
+        row_selection: Option<Vec<RowRange>>,
+        page_index_enabled: Option<bool>,
+    ) -> (usize, u64) {
         let file_size = data.len() as u64;
         let file_read = TrackingFileRead::new(data);
         let tracker = file_read.clone();
@@ -3634,7 +3764,11 @@ mod tests {
             "items".to_string(),
             DataType::Array(ArrayType::new(DataType::Int(IntType::new()))),
         )];
-        let stream = ParquetFormatReader::default()
+        let reader = match page_index_enabled {
+            Some(enabled) => 
ParquetFormatReader::default().with_page_index_enabled(enabled),
+            None => ParquetFormatReader::default(),
+        };
+        let stream = reader
             .read_batch_stream(
                 Box::new(file_read),
                 file_size,
@@ -3659,15 +3793,26 @@ mod tests {
     #[tokio::test]
     async fn test_row_selection_prunes_nested_page_io() {
         let data = Bytes::from(write_nested_multi_page_parquet().await);
-        let (all_rows, all_bytes) = read_nested_rows(data.clone(), None).await;
-        let (selected_rows, selected_bytes) =
-            read_nested_rows(data, Some(vec![RowRange::new(0, 0)])).await;
+        let (all_rows, all_bytes) = read_nested_rows(data.clone(), None, 
None).await;
+        let selection = Some(vec![RowRange::new(0, 0)]);
+        let (default_rows, default_bytes) =
+            read_nested_rows(data.clone(), selection.clone(), None).await;
+        let (enabled_rows, enabled_bytes) =
+            read_nested_rows(data.clone(), selection.clone(), 
Some(true)).await;
+        let (disabled_rows, disabled_bytes) = read_nested_rows(data, 
selection, Some(false)).await;
 
         assert_eq!(all_rows, 1024);
-        assert_eq!(selected_rows, 1);
+        assert_eq!(default_rows, 1);
+        assert_eq!(enabled_rows, 1);
+        assert_eq!(disabled_rows, 1);
+        assert_eq!(default_bytes, enabled_bytes);
+        assert!(
+            enabled_bytes * 2 < disabled_bytes,
+            "page-index read used {enabled_bytes} bytes; disabled read used 
{disabled_bytes} bytes"
+        );
         assert!(
-            selected_bytes * 2 < all_bytes,
-            "selected read used {selected_bytes} bytes; full read used 
{all_bytes} bytes"
+            enabled_bytes * 2 < all_bytes,
+            "selected read used {enabled_bytes} bytes; full read used 
{all_bytes} bytes"
         );
     }
 
diff --git a/crates/paimon/src/spec/core_options.rs 
b/crates/paimon/src/spec/core_options.rs
index a741a087..58131b6c 100644
--- a/crates/paimon/src/spec/core_options.rs
+++ b/crates/paimon/src/spec/core_options.rs
@@ -92,6 +92,7 @@ const MANIFEST_SIDECAR_ENABLED_OPTION: &str = 
"manifest.sidecar.enabled";
 const MANIFEST_SORT_ENABLED_OPTION: &str = "manifest-sort.enabled";
 const WRITE_PARQUET_BUFFER_SIZE_OPTION: &str = "write.parquet-buffer-size";
 const READ_BATCH_SIZE_OPTION: &str = "read.batch-size";
+const PARQUET_FILTER_COLUMN_INDEX_ENABLED_OPTION: &str = 
"parquet.filter.columnindex.enabled";
 const PARQUET_ROW_GROUP_PARALLELISM_OPTION: &str = 
"read.parquet.row-group.parallelism";
 pub(crate) const PARQUET_ROW_GROUP_MAX_INFLIGHT_BYTES_OPTION: &str =
     "read.parquet.row-group.max-inflight-bytes";
@@ -733,6 +734,22 @@ impl<'a> CoreOptions<'a> {
             .unwrap_or(true)
     }
 
+    /// Whether Parquet page indexes are used for page pruning. Default is 
true.
+    pub fn parquet_filter_column_index_enabled(&self) -> crate::Result<bool> {
+        let Some(raw) = 
self.options.get(PARQUET_FILTER_COLUMN_INDEX_ENABLED_OPTION) else {
+            return Ok(true);
+        };
+        match raw.trim().to_ascii_lowercase().as_str() {
+            "true" => Ok(true),
+            "false" => Ok(false),
+            _ => Err(crate::Error::ConfigInvalid {
+                message: format!(
+                    "Option '{PARQUET_FILTER_COLUMN_INDEX_ENABLED_OPTION}' 
must be true or false, got: {raw}"
+                ),
+            }),
+        }
+    }
+
     /// The declared [`TableType`], defaulting to [`TableType::Table`].
     /// Fails on a value this client does not know.
     pub fn table_type(&self) -> crate::Result<TableType> {
@@ -1788,6 +1805,38 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_parquet_filter_column_index_enabled() {
+        let options = HashMap::new();
+        assert!(CoreOptions::new(&options)
+            .parquet_filter_column_index_enabled()
+            .unwrap());
+
+        for (value, expected) in [("true", true), ("false", false), (" FALSE 
", false)] {
+            let options = HashMap::from([(
+                PARQUET_FILTER_COLUMN_INDEX_ENABLED_OPTION.to_string(),
+                value.to_string(),
+            )]);
+            assert_eq!(
+                CoreOptions::new(&options)
+                    .parquet_filter_column_index_enabled()
+                    .unwrap(),
+                expected
+            );
+        }
+
+        let options = HashMap::from([(
+            PARQUET_FILTER_COLUMN_INDEX_ENABLED_OPTION.to_string(),
+            "invalid".to_string(),
+        )]);
+        let error = CoreOptions::new(&options)
+            .parquet_filter_column_index_enabled()
+            .unwrap_err();
+        assert!(error
+            .to_string()
+            .contains("parquet.filter.columnindex.enabled"));
+    }
+
     #[test]
     fn test_source_split_defaults() {
         let options = HashMap::new();
diff --git a/crates/paimon/src/spec/schema.rs b/crates/paimon/src/spec/schema.rs
index 662889ad..11dc4365 100644
--- a/crates/paimon/src/spec/schema.rs
+++ b/crates/paimon/src/spec/schema.rs
@@ -2520,6 +2520,27 @@ mod tests {
         assert!(copied.core_options().index_file_in_data_file_dir());
     }
 
+    #[test]
+    fn a_dynamic_copy_can_disable_parquet_page_indexes() {
+        let stored = TableSchema::new(
+            0,
+            &Schema::builder()
+                .column("id", DataType::Int(IntType::new()))
+                .option("parquet.filter.columnindex.enabled", "true")
+                .build()
+                .unwrap(),
+        );
+        let copied = stored.copy_with_options(HashMap::from([(
+            "parquet.filter.columnindex.enabled".to_string(),
+            "false".to_string(),
+        )]));
+
+        assert!(!copied
+            .core_options()
+            .parquet_filter_column_index_enabled()
+            .unwrap());
+    }
+
     #[test]
     fn test_copy_with_replaced_options() {
         let schema = Schema::builder()
diff --git a/crates/paimon/src/table/audit_log_table/read.rs 
b/crates/paimon/src/table/audit_log_table/read.rs
index 5b4aac43..175ce60e 100644
--- a/crates/paimon/src/table/audit_log_table/read.rs
+++ b/crates/paimon/src/table/audit_log_table/read.rs
@@ -104,6 +104,7 @@ impl<'a> AuditLogRead<'a> {
             
.with_file_index_read_enabled(core_options.file_index_read_enabled())
             .with_batch_size(Some(core_options.read_batch_size()?))
             .with_parquet_read_budget(Some(Arc::clone(&parquet_read_budget)))
+            .with_table_options(self.read.table.schema().options().clone())
             .read(&raw_splits)?;
             let merge_stream = KeyValueFileReader::new(
                 self.read.table.file_io.clone(),
diff --git a/crates/paimon/src/table/data_evolution_reader.rs 
b/crates/paimon/src/table/data_evolution_reader.rs
index ce0ece4d..ddd80975 100644
--- a/crates/paimon/src/table/data_evolution_reader.rs
+++ b/crates/paimon/src/table/data_evolution_reader.rs
@@ -117,6 +117,7 @@ pub(crate) struct DataEvolutionReader {
     blob_parallelism: usize,
     batch_size: Option<usize>,
     parquet_read_budget: Option<Arc<ReadBudget>>,
+    table_options: Arc<HashMap<String, String>>,
     mosaic_prefetch: MosaicPrefetchOptions,
     read_timing: Option<Arc<DataFileReadTiming>>,
 }
@@ -197,6 +198,7 @@ impl DataEvolutionReader {
             blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
             batch_size: None,
             parquet_read_budget: None,
+            table_options: Arc::new(HashMap::new()),
             mosaic_prefetch: MosaicPrefetchOptions::default(),
             read_timing: None,
         })
@@ -222,6 +224,14 @@ impl DataEvolutionReader {
         self
     }
 
+    pub(crate) fn with_table_options(
+        mut self,
+        options: impl Into<Arc<HashMap<String, String>>>,
+    ) -> Self {
+        self.table_options = options.into();
+        self
+    }
+
     pub(crate) fn with_mosaic_prefetch(mut self, mosaic_prefetch: 
MosaicPrefetchOptions) -> Self {
         self.mosaic_prefetch = mosaic_prefetch;
         self
@@ -275,6 +285,7 @@ impl DataEvolutionReader {
             .with_batch_size(self.batch_size)
             .with_blob_parallelism(self.blob_parallelism)
             .with_parquet_read_budget(self.parquet_read_budget.clone())
+            .with_table_options(Arc::clone(&self.table_options))
             .with_mosaic_prefetch(self.mosaic_prefetch)
             .with_read_timing(self.read_timing.clone());
 
@@ -576,6 +587,7 @@ impl DataEvolutionReader {
         )?
         .with_batch_size(self.batch_size)
         .with_parquet_read_budget(self.parquet_read_budget.clone())
+        .with_table_options(Arc::clone(&self.table_options))
         .with_mosaic_prefetch(self.mosaic_prefetch);
         let mut stream = prescan.read(splits)?;
         let mut view_structs = HashSet::new();
@@ -641,6 +653,7 @@ impl DataEvolutionReader {
         let blob_parallelism = self.blob_parallelism;
         let batch_size = self.batch_size;
         let parquet_read_budget = self.parquet_read_budget.clone();
+        let table_options = Arc::clone(&self.table_options);
         let mosaic_prefetch = self.mosaic_prefetch;
         let read_timing = self.read_timing.clone();
         let anchor_deletion_vector = anchor_deletion_vector.clone();
@@ -730,6 +743,7 @@ impl DataEvolutionReader {
                             blob_as_descriptor,
                             blob_parallelism,
                             source_parquet_read_budget.clone(),
+                            Arc::clone(&table_options),
                             mosaic_prefetch,
                             read_timing.clone(),
                             anchor_deletion_vector.as_ref(),
@@ -1271,6 +1285,7 @@ fn open_source_stream(
     blob_as_descriptor: bool,
     blob_parallelism: usize,
     parquet_read_budget: Option<Arc<ReadBudget>>,
+    table_options: Arc<HashMap<String, String>>,
     mosaic_prefetch: MosaicPrefetchOptions,
     read_timing: Option<Arc<DataFileReadTiming>>,
     anchor_deletion_vector: Option<&DeletionVectorContext>,
@@ -1341,6 +1356,7 @@ fn open_source_stream(
     .with_blob_as_descriptor(blob_as_descriptor)
     .with_blob_parallelism(blob_parallelism)
     .with_parquet_read_budget(parquet_read_budget)
+    .with_table_options(table_options)
     .with_mosaic_prefetch(mosaic_prefetch)
     .with_read_timing(read_timing);
 
diff --git a/crates/paimon/src/table/data_file_reader.rs 
b/crates/paimon/src/table/data_file_reader.rs
index 7019e9bc..d72703b0 100644
--- a/crates/paimon/src/table/data_file_reader.rs
+++ b/crates/paimon/src/table/data_file_reader.rs
@@ -36,6 +36,7 @@ use arrow_array::{Array, Int64Array, RecordBatch};
 use async_stream::try_stream;
 use futures::StreamExt;
 use roaring::RoaringBitmap;
+use std::collections::HashMap;
 use std::ops::Range;
 use std::sync::atomic::{AtomicU64, Ordering};
 use std::sync::Arc;
@@ -121,6 +122,7 @@ pub(crate) struct DataFileReader {
     blob_parallelism: usize,
     batch_size: Option<usize>,
     parquet_read_budget: Option<Arc<ReadBudget>>,
+    table_options: Arc<HashMap<String, String>>,
     mosaic_prefetch: MosaicPrefetchOptions,
     read_timing: Option<Arc<DataFileReadTiming>>,
 }
@@ -147,6 +149,7 @@ impl DataFileReader {
             blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
             batch_size: None,
             parquet_read_budget: None,
+            table_options: Arc::new(HashMap::new()),
             mosaic_prefetch: MosaicPrefetchOptions::default(),
             read_timing: None,
         }
@@ -181,6 +184,14 @@ impl DataFileReader {
         self
     }
 
+    pub(crate) fn with_table_options(
+        mut self,
+        options: impl Into<Arc<HashMap<String, String>>>,
+    ) -> Self {
+        self.table_options = options.into();
+        self
+    }
+
     pub(crate) fn with_mosaic_prefetch(mut self, mosaic_prefetch: 
MosaicPrefetchOptions) -> Self {
         self.mosaic_prefetch = mosaic_prefetch;
         self
@@ -463,10 +474,11 @@ impl DataFileReader {
             .flatten();
         let file_io = self.file_io.clone();
         let split = split.clone();
+        let batch_size = self.batch_size;
         let blob_as_descriptor = self.blob_as_descriptor;
         let blob_parallelism = self.blob_parallelism;
-        let batch_size = self.batch_size;
         let parquet_read_budget = self.parquet_read_budget.clone();
+        let table_options = Arc::clone(&self.table_options);
         let mosaic_prefetch = self.mosaic_prefetch;
         let read_timing = self.read_timing.clone();
 
@@ -526,6 +538,7 @@ impl DataFileReader {
                 &path_to_read,
                 blob_as_descriptor,
                 &format_read_fields,
+                &table_options,
                 parquet_read_budget,
                 blob_parallelism,
                 mosaic_prefetch,
@@ -738,6 +751,7 @@ impl DataFileReader {
         let blob_as_descriptor = self.blob_as_descriptor;
         let blob_parallelism = self.blob_parallelism;
         let parquet_read_budget = self.parquet_read_budget.clone();
+        let table_options = Arc::clone(&self.table_options);
         let mosaic_prefetch = self.mosaic_prefetch;
 
         let target_schema = build_target_arrow_schema(&read_type)?;
@@ -802,6 +816,7 @@ impl DataFileReader {
                 &path_to_read,
                 blob_as_descriptor,
                 &format_read_fields,
+                &table_options,
                 parquet_read_budget,
                 blob_parallelism,
                 mosaic_prefetch,
diff --git a/crates/paimon/src/table/format_table_read.rs 
b/crates/paimon/src/table/format_table_read.rs
index d2281405..e419e78e 100644
--- a/crates/paimon/src/table/format_table_read.rs
+++ b/crates/paimon/src/table/format_table_read.rs
@@ -130,6 +130,7 @@ impl<'a> FormatTableRead<'a> {
         let mosaic_prefetch = configured_mosaic_prefetch(self.table)?;
         let row_filter_factory = self.row_filter_factory.clone();
         let parquet_read_budget = Some(self.parquet_read_budget()?);
+        let table_options = self.table.schema().options().clone();
         let blob_parallelism = self.blob_parallelism;
 
         Ok(try_stream! {
@@ -149,6 +150,7 @@ impl<'a> FormatTableRead<'a> {
                 .with_batch_size(batch_size)
                 .with_blob_parallelism(blob_parallelism)
                 .with_parquet_read_budget(parquet_read_budget.clone())
+                .with_table_options(table_options.clone())
                 .with_mosaic_prefetch(mosaic_prefetch);
                 if let Some(factory) = &row_filter_factory {
                     reader = 
reader.with_row_filter_factory(Arc::clone(factory));
diff --git a/crates/paimon/src/table/full_text_search_builder.rs 
b/crates/paimon/src/table/full_text_search_builder.rs
index fac4db4a..1c567162 100644
--- a/crates/paimon/src/table/full_text_search_builder.rs
+++ b/crates/paimon/src/table/full_text_search_builder.rs
@@ -273,7 +273,8 @@ impl<'a> FullTextSearchBuilder<'a> {
             self.table.schema().fields().to_vec(),
             self.table.schema().fields().to_vec(),
             Vec::new(),
-        );
+        )
+        .with_table_options(self.table.schema().options().clone());
         let read = PrimaryKeyFullTextRead::new(
             self.table.file_io().clone(),
             materialize_reader,
diff --git a/crates/paimon/src/table/hybrid_search_builder.rs 
b/crates/paimon/src/table/hybrid_search_builder.rs
index 795978b7..f6be569f 100644
--- a/crates/paimon/src/table/hybrid_search_builder.rs
+++ b/crates/paimon/src/table/hybrid_search_builder.rs
@@ -492,7 +492,8 @@ impl<'a> HybridSearchBuilder<'a> {
             self.table.schema().fields().to_vec(),
             self.table.schema().fields().to_vec(),
             Vec::new(),
-        );
+        )
+        .with_table_options(self.table.schema().options().clone());
 
         let mut batches: Vec<RecordBatch> = Vec::new();
         let mut ranked: Vec<RankedRow> = Vec::new();
@@ -634,7 +635,8 @@ impl<'a> HybridSearchBuilder<'a> {
             table.schema().fields().to_vec(),
             table.schema().fields().to_vec(),
             Vec::new(),
-        );
+        )
+        .with_table_options(table.schema().options().clone());
         let read = PrimaryKeyFullTextRead::new(
             table.file_io().clone(),
             materialize_reader,
diff --git a/crates/paimon/src/table/kv_file_reader.rs 
b/crates/paimon/src/table/kv_file_reader.rs
index 0bcc97cd..9b2a868f 100644
--- a/crates/paimon/src/table/kv_file_reader.rs
+++ b/crates/paimon/src/table/kv_file_reader.rs
@@ -592,6 +592,7 @@ impl KeyValueFileReader {
                         )
                         .with_batch_size(Some(config.read_batch_size))
                         
.with_parquet_read_budget(group_parquet_read_budget.clone())
+                        .with_table_options(config.table_options.clone())
                         .with_mosaic_prefetch(config.mosaic_prefetch);
                         let run_schema_manager = config.schema_manager.clone();
                         let run_file_io = file_io.clone();
diff --git a/crates/paimon/src/table/pk_vector_read.rs 
b/crates/paimon/src/table/pk_vector_read.rs
index 8a4ab204..59f74c44 100644
--- a/crates/paimon/src/table/pk_vector_read.rs
+++ b/crates/paimon/src/table/pk_vector_read.rs
@@ -300,7 +300,8 @@ async fn search_pk_raw_candidates_batch_with_plan(
         table.schema().fields().to_vec(),
         vec![vector_field.clone()],
         Vec::new(),
-    );
+    )
+    .with_table_options(table.schema().options().clone());
 
     // Real ANN scorer + loader. Each segment source is opened lazily inside 
its
     // bucket leaf and dropped after scoring. Lumina keeps its buffered-byte 
path;
@@ -447,7 +448,8 @@ async fn search_pk_raw_candidates_batch_with_plan(
                     table.schema().fields().to_vec(),
                     residual_read_type,
                     Vec::new(),
-                );
+                )
+                .with_table_options(table.schema().options().clone());
                 let mut per_split = Vec::with_capacity(plan.splits.len());
                 for (index, split) in plan.splits.iter().enumerate() {
                     // The plan's selection for this split, so the residual is
@@ -611,7 +613,8 @@ async fn search_pk_candidates_batch_with_plan(
                 table.schema().fields().to_vec(),
                 vec![vector_field.clone()],
                 Vec::new(),
-            );
+            )
+            .with_table_options(table.schema().options().clone());
             rerank_indexed_positional(
                 &rerank_reader,
                 search.indexed,
diff --git a/crates/paimon/src/table/table_read.rs 
b/crates/paimon/src/table/table_read.rs
index 47a0b0a7..d232614b 100644
--- a/crates/paimon/src/table/table_read.rs
+++ b/crates/paimon/src/table/table_read.rs
@@ -458,6 +458,7 @@ impl<'a> PaimonTableRead<'a> {
         .with_batch_size(Some(core_options.read_batch_size()?))
         .with_blob_parallelism(self.blob_parallelism)
         .with_parquet_read_budget(Some(self.parquet_read_budget()?))
+        .with_table_options(self.table.schema().options().clone())
         .with_mosaic_prefetch(configured_mosaic_prefetch(self.table)?);
         let raw_stream = reader.read(&data_splits)?;
 
@@ -941,6 +942,7 @@ impl<'a> PaimonTableRead<'a> {
         .with_batch_size(Some(core_options.read_batch_size()?))
         .with_blob_parallelism(self.blob_parallelism)
         .with_parquet_read_budget(Some(self.parquet_read_budget()?))
+        .with_table_options(self.table.schema().options().clone())
         .with_mosaic_prefetch(configured_mosaic_prefetch(self.table)?)
         .with_read_timing(self.data_file_read_timing.clone());
         reader.read(data_splits)
@@ -965,6 +967,7 @@ impl<'a> PaimonTableRead<'a> {
         .with_batch_size(Some(core_options.read_batch_size()?))
         .with_blob_parallelism(self.blob_parallelism)
         .with_parquet_read_budget(Some(self.parquet_read_budget()?))
+        .with_table_options(self.table.schema().options().clone())
         .with_mosaic_prefetch(configured_mosaic_prefetch(self.table)?)
         .with_read_timing(self.data_file_read_timing.clone());
         // The engine decoder filter is safe only on the plain append/raw path.
diff --git a/crates/paimon/src/table/vector_search_result.rs 
b/crates/paimon/src/table/vector_search_result.rs
index 5ceae40b..0a69ba45 100644
--- a/crates/paimon/src/table/vector_search_result.rs
+++ b/crates/paimon/src/table/vector_search_result.rs
@@ -201,7 +201,8 @@ impl SearchResultReadBuilder<'_> {
 
     pub async fn read(&self) -> crate::Result<ArrowRecordBatchStream> {
         let table = &self.result.table;
-        CoreOptions::new(table.schema().options()).ensure_read_authorized()?;
+        let core_options = CoreOptions::new(table.schema().options());
+        core_options.ensure_read_authorized()?;
         match &self.result.hits {
             SearchHits::DataEvolution {
                 vector_column,
@@ -220,7 +221,8 @@ impl SearchResultReadBuilder<'_> {
                     table.schema().fields().to_vec(),
                     read_type,
                     Vec::new(),
-                );
+                )
+                .with_table_options(table.schema().options().clone());
                 materialize_positions(positions, splits, &reader).await
             }
         }
diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md
index c04e54b9..214d9d39 100644
--- a/docs/src/getting-started.md
+++ b/docs/src/getting-started.md
@@ -122,6 +122,13 @@ cross-backend rename is rejected.
 with `with_provider` on the same builder. Without a provider, property-based
 storage configuration behaves as before.
 
+## Parquet Page Pruning
+
+Parquet Page Index pruning is enabled by default. Set
+`parquet.filter.columnindex.enabled=false` to avoid fine-grained range 
requests;
+this may reduce request count while increasing bytes read. The option does not
+change Page Index generation.
+
 ## Mosaic File Format
 
 Mosaic data file reads are always available. The current Mosaic support is 
read-only: Paimon Rust can read existing `.mosaic` data files, including array 
and map columns, in a Paimon table, but it does not write Mosaic data files yet.

Reply via email to