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 539cf430 feat(python): configure native BLOB read parallelism (#871)
539cf430 is described below
commit 539cf430806feddbea7fb6b60494fae4921009b3
Author: Jingsong Lee <[email protected]>
AuthorDate: Fri Sep 18 16:27:06 2026 +0800
feat(python): configure native BLOB read parallelism (#871)
---
.../python/python/pypaimon_rust/datafusion.pyi | 3 +
bindings/python/src/read.rs | 24 ++++++
bindings/python/tests/test_read.py | 13 ++++
bindings/python/tests/test_write.py | 4 +-
crates/paimon/src/arrow/format/blob.rs | 90 ++++++++++++++++++++--
crates/paimon/src/arrow/format/mod.rs | 17 ++--
crates/paimon/src/table/blob_resolver.rs | 54 ++++++++++++-
crates/paimon/src/table/data_evolution_reader.rs | 33 ++++++--
.../table/data_evolution_reader/blob_fallback.rs | 16 +++-
crates/paimon/src/table/data_file_reader.rs | 13 ++++
crates/paimon/src/table/format_read_builder.rs | 13 +++-
crates/paimon/src/table/format_table_read.rs | 10 +++
crates/paimon/src/table/read_builder.rs | 34 +++++++-
crates/paimon/src/table/table_read.rs | 45 ++++++++++-
14 files changed, 335 insertions(+), 34 deletions(-)
diff --git a/bindings/python/python/pypaimon_rust/datafusion.pyi
b/bindings/python/python/pypaimon_rust/datafusion.pyi
index e9aadcf6..8bf6b3cd 100644
--- a/bindings/python/python/pypaimon_rust/datafusion.pyi
+++ b/bindings/python/python/pypaimon_rust/datafusion.pyi
@@ -84,6 +84,9 @@ class ReadBuilder:
"""Set a scan-planning row-limit hint, not an exact cap: a matching
split is
returned whole. Apply application-level limiting for an exact bound."""
...
+ def with_blob_parallelism(self, blob_parallelism: int) -> "ReadBuilder":
+ """Set the maximum number of concurrent BLOB range reads. Must be
positive."""
+ ...
def with_filter(self, predicate: dict) -> "ReadBuilder": ...
def with_row_ranges(self, ranges: Sequence[tuple[int, int]]) ->
"ReadBuilder":
"""Set Data Evolution row ranges. Empty selects no rows; format tables
are unsupported."""
diff --git a/bindings/python/src/read.rs b/bindings/python/src/read.rs
index 0a5ab881..42c89165 100644
--- a/bindings/python/src/read.rs
+++ b/bindings/python/src/read.rs
@@ -112,6 +112,7 @@ pub struct PyReadBuilder {
filter: Option<Predicate>,
row_ranges: Option<Vec<RowRange>>,
case_sensitive: bool,
+ blob_parallelism: Option<usize>,
}
impl PyReadBuilder {
@@ -123,6 +124,7 @@ impl PyReadBuilder {
filter: None,
row_ranges: None,
case_sensitive: true,
+ blob_parallelism: None,
}
}
@@ -168,6 +170,7 @@ impl PyReadBuilder {
filter: None,
row_ranges: None,
case_sensitive: true,
+ blob_parallelism: None,
})
}
}
@@ -184,6 +187,20 @@ impl PyReadBuilder {
slf
}
+ /// Set the maximum number of concurrent BLOB range reads for this read.
+ fn with_blob_parallelism(
+ mut slf: PyRefMut<'_, Self>,
+ blob_parallelism: usize,
+ ) -> PyResult<PyRefMut<'_, Self>> {
+ if blob_parallelism == 0 {
+ return Err(PyValueError::new_err(
+ "blob_parallelism must be greater than zero",
+ ));
+ }
+ slf.blob_parallelism = Some(blob_parallelism);
+ Ok(slf)
+ }
+
/// Set whether column-name matching (projection and predicate resolution)
is
/// case-sensitive. Defaults to `true` (exact match); pass `false` to match
/// column names case-insensitively (ASCII case-folding).
@@ -256,6 +273,7 @@ impl PyReadBuilder {
limit: self.limit,
filter: self.filter.clone(),
case_sensitive: self.case_sensitive,
+ blob_parallelism: self.blob_parallelism,
}
}
}
@@ -378,6 +396,7 @@ pub struct PyTableRead {
limit: Option<usize>,
filter: Option<Predicate>,
case_sensitive: bool,
+ blob_parallelism: Option<usize>,
}
impl PyTableRead {
@@ -396,6 +415,11 @@ impl PyTableRead {
&self.filter,
self.case_sensitive,
)?;
+ if let Some(blob_parallelism) = self.blob_parallelism {
+ builder
+ .with_blob_parallelism(blob_parallelism)
+ .map_err(to_py_err)?;
+ }
// Validate config (e.g. projection) before the empty-splits fast
// path so an invalid projection fails consistently regardless of
// how many splits are passed.
diff --git a/bindings/python/tests/test_read.py
b/bindings/python/tests/test_read.py
index 448cc47a..e772f73d 100644
--- a/bindings/python/tests/test_read.py
+++ b/bindings/python/tests/test_read.py
@@ -66,6 +66,19 @@ def test_with_limit():
assert plan is not None
+def test_with_blob_parallelism():
+ with tempfile.TemporaryDirectory() as warehouse:
+ table = _make_table_with_data(warehouse)
+ builder = table.new_read_builder().with_blob_parallelism(2)
+ plan = builder.new_scan().plan()
+ assert pa.Table.from_batches(
+ builder.new_read().read(plan.splits())
+ ).num_rows == 3
+
+ with pytest.raises(ValueError, match="must be greater than zero"):
+ table.new_read_builder().with_blob_parallelism(0)
+
+
def test_with_row_ranges():
with tempfile.TemporaryDirectory() as warehouse:
ctx = SQLContext()
diff --git a/bindings/python/tests/test_write.py
b/bindings/python/tests/test_write.py
index ed5be8b3..98d9274d 100644
--- a/bindings/python/tests/test_write.py
+++ b/bindings/python/tests/test_write.py
@@ -299,7 +299,9 @@ def test_ltz_schema_alias_write_roundtrip(tmp_path,
precision, unit, micros, fra
schema["fields"][1]["type"] = "TIMESTAMP_LTZ({})".format(precision)
schema_path.write_text(json.dumps(schema))
table = _get_table(str(tmp_path))
- assert "LocalZonedTimestamp" in table.schema().fields()[1].field_type()
+ assert table.schema().fields()[1].field_type() == (
+ f"TIMESTAMP({precision}) WITH LOCAL TIME ZONE"
+ )
value = datetime(2026, 9, 15, 20, 0, 0, micros, tzinfo=timezone.utc)
arrow_schema = pa.schema([("id", pa.int32()), ("ts", pa.timestamp(unit,
tz="UTC"))])
builder = table.new_write_builder()
diff --git a/crates/paimon/src/arrow/format/blob.rs
b/crates/paimon/src/arrow/format/blob.rs
index 6002d465..f03db782 100644
--- a/crates/paimon/src/arrow/format/blob.rs
+++ b/crates/paimon/src/arrow/format/blob.rs
@@ -39,6 +39,7 @@ use std::sync::Arc;
pub(crate) struct BlobFormatReader {
descriptor_mode: bool,
file_path: String,
+ blob_parallelism: usize,
}
impl BlobFormatReader {
@@ -46,8 +47,15 @@ impl BlobFormatReader {
Self {
descriptor_mode,
file_path,
+ blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
}
}
+
+ pub(crate) fn with_blob_parallelism(mut self, blob_parallelism: usize) ->
Self {
+ debug_assert!(blob_parallelism > 0);
+ self.blob_parallelism = blob_parallelism;
+ self
+ }
}
pub(crate) struct IndexedBlobReader {
@@ -55,21 +63,42 @@ pub(crate) struct IndexedBlobReader {
index: BlobFileIndex,
descriptor_mode: bool,
file_path: String,
+ blob_parallelism: usize,
}
impl IndexedBlobReader {
+ #[cfg(test)]
pub(crate) async fn open(
reader: Box<dyn FileRead>,
file_size: u64,
file_path: String,
descriptor_mode: bool,
) -> crate::Result<Self> {
+ Self::open_with_parallelism(
+ reader,
+ file_size,
+ file_path,
+ descriptor_mode,
+ DEFAULT_BLOB_READ_PARALLELISM,
+ )
+ .await
+ }
+
+ pub(crate) async fn open_with_parallelism(
+ reader: Box<dyn FileRead>,
+ file_size: u64,
+ file_path: String,
+ descriptor_mode: bool,
+ blob_parallelism: usize,
+ ) -> crate::Result<Self> {
+ debug_assert!(blob_parallelism > 0);
let index = BlobFileIndex::load(reader.as_ref(), file_size).await?;
Ok(Self {
reader,
index,
descriptor_mode,
file_path,
+ blob_parallelism,
})
}
@@ -85,7 +114,7 @@ impl IndexedBlobReader {
build_descriptor_values(&self.index, positions, &self.file_path)
} else {
let planned_reads = plan_blob_reads(&self.index, positions)?;
- fetch_blob_values(self.reader.as_ref(), planned_reads).await
+ fetch_blob_values(self.reader.as_ref(), planned_reads,
self.blob_parallelism).await
}
}
@@ -99,6 +128,7 @@ impl IndexedBlobReader {
planned_reads,
&self.file_path,
self.descriptor_mode,
+ self.blob_parallelism,
)
.await
}
@@ -115,6 +145,7 @@ impl IndexedBlobReader {
&self.file_path,
self.descriptor_mode,
key_type,
+ self.blob_parallelism,
)
.await
}
@@ -137,7 +168,7 @@ const BLOB_INLINE_HEADER_SIZE: u64 = 4;
const BLOB_TRAILER_SIZE: u64 = 12;
const BLOB_ENTRY_OVERHEAD: u64 = BLOB_INLINE_HEADER_SIZE + BLOB_TRAILER_SIZE;
const DEFAULT_BATCH_SIZE: usize = 128;
-const BLOB_READ_CONCURRENCY: usize = 8;
+pub(crate) const DEFAULT_BLOB_READ_PARALLELISM: usize = 8;
const BLOB_ARRAY_MAGIC_NUMBER: i32 = 1094861634;
const BLOB_ARRAY_VERSION: u8 = 1;
const BLOB_ARRAY_HEADER_SIZE: u64 = 9;
@@ -178,11 +209,12 @@ impl FormatFileReader for BlobFormatReader {
let target_schema = build_target_arrow_schema(read_fields)?;
let batch_size = batch_size.unwrap_or(DEFAULT_BATCH_SIZE);
- let blob_reader = IndexedBlobReader::open(
+ let blob_reader = IndexedBlobReader::open_with_parallelism(
reader,
file_size,
self.file_path.clone(),
self.descriptor_mode,
+ self.blob_parallelism,
)
.await?;
let mut selection = RowSelectionCursor::new(blob_reader.num_rows(),
row_selection)?;
@@ -653,6 +685,7 @@ fn plan_blob_reads(
async fn fetch_blob_values(
reader: &dyn FileRead,
planned_reads: Vec<PlannedBlobRead>,
+ blob_parallelism: usize,
) -> crate::Result<Vec<BlobReadValue>> {
futures::stream::iter(planned_reads.into_iter().map(|planned_read| async
move {
match planned_read {
@@ -663,7 +696,7 @@ async fn fetch_blob_values(
.map(BlobReadValue::Value),
}
}))
- .buffered(BLOB_READ_CONCURRENCY)
+ .buffered(blob_parallelism)
.try_collect()
.await
}
@@ -759,6 +792,7 @@ async fn fetch_blob_array_values(
planned_reads: Vec<PlannedBlobArrayRead>,
file_path: &str,
descriptor_mode: bool,
+ blob_parallelism: usize,
) -> crate::Result<Vec<BlobReadValue>> {
futures::stream::iter(planned_reads.into_iter().map(|planned_read| async
move {
match planned_read {
@@ -774,7 +808,7 @@ async fn fetch_blob_array_values(
}
}
}))
- .buffered(BLOB_READ_CONCURRENCY)
+ .buffered(blob_parallelism)
.try_collect()
.await
}
@@ -1076,6 +1110,7 @@ async fn fetch_blob_map_values(
file_path: &str,
descriptor_mode: bool,
key_type: &DataType,
+ blob_parallelism: usize,
) -> crate::Result<Vec<BlobReadValue>> {
futures::stream::iter(planned_reads.into_iter().map(|planned_read| async
move {
match planned_read {
@@ -1087,7 +1122,7 @@ async fn fetch_blob_map_values(
}
}
}))
- .buffered(BLOB_READ_CONCURRENCY)
+ .buffered(blob_parallelism)
.try_collect()
.await
}
@@ -2446,7 +2481,48 @@ mod tests {
.collect::<Vec<_>>()
);
assert!(reader.max_in_flight() > 1);
- assert!(reader.max_in_flight() <= BLOB_READ_CONCURRENCY);
+ assert!(reader.max_in_flight() <= DEFAULT_BLOB_READ_PARALLELISM);
+ }
+
+ #[tokio::test]
+ async fn test_blob_reader_honors_configured_parallelism() {
+ let payloads = (0_u8..12).map(|value| vec![value]).collect::<Vec<_>>();
+ let rows = payloads
+ .iter()
+ .map(|payload| Some(payload.as_slice()))
+ .collect::<Vec<_>>();
+ let file_bytes = blob_test_utils::build_blob_file_bytes(&rows);
+ let read_fields = vec![DataField::new(
+ 0,
+ "payload".to_string(),
+ DataType::Blob(BlobType::new()),
+ )];
+ let reader = TrackingFileRead::new(Bytes::from(file_bytes.clone()));
+
+ let batches = BlobFormatReader::new(String::new(), false)
+ .with_blob_parallelism(2)
+ .read_batch_stream(
+ Box::new(reader.clone()),
+ file_bytes.len() as u64,
+ &read_fields,
+ None,
+ Some(12),
+ None,
+ )
+ .await
+ .unwrap()
+ .try_collect::<Vec<_>>()
+ .await
+ .unwrap();
+
+ assert_eq!(batches.len(), 1);
+ assert_eq!(
+ collect_binary_values(&batches[0]),
+ (0_u8..12)
+ .map(|value| Some(vec![value]))
+ .collect::<Vec<_>>()
+ );
+ assert_eq!(reader.max_in_flight(), 2);
}
#[tokio::test]
diff --git a/crates/paimon/src/arrow/format/mod.rs
b/crates/paimon/src/arrow/format/mod.rs
index f67ae565..17d21c9f 100644
--- a/crates/paimon/src/arrow/format/mod.rs
+++ b/crates/paimon/src/arrow/format/mod.rs
@@ -190,7 +190,13 @@ pub(crate) fn create_format_reader(
blob_as_descriptor: bool,
read_fields: &[DataField],
) -> crate::Result<Box<dyn FormatFileReader>> {
- create_format_reader_with_budget(path, blob_as_descriptor, read_fields,
None)
+ create_format_reader_with_budget(
+ path,
+ blob_as_descriptor,
+ read_fields,
+ None,
+ blob::DEFAULT_BLOB_READ_PARALLELISM,
+ )
}
/// Create a format reader with a scan-shared Parquet resource budget.
@@ -199,6 +205,7 @@ pub(crate) fn create_format_reader_with_budget(
blob_as_descriptor: bool,
read_fields: &[DataField],
parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ blob_parallelism: usize,
) -> crate::Result<Box<dyn FormatFileReader>> {
let lower = path.to_ascii_lowercase();
let reader: Box<dyn FormatFileReader> = if lower.ends_with(".parquet") {
@@ -207,10 +214,10 @@ pub(crate) fn create_format_reader_with_budget(
None => parquet::ParquetFormatReader::default(),
})
} else if lower.ends_with(".blob") {
- Box::new(blob::BlobFormatReader::new(
- path.to_string(),
- blob_as_descriptor,
- ))
+ Box::new(
+ blob::BlobFormatReader::new(path.to_string(), blob_as_descriptor)
+ .with_blob_parallelism(blob_parallelism),
+ )
} else if lower.ends_with(".orc") {
Box::new(orc::OrcFormatReader)
} else if lower.ends_with(".avro") {
diff --git a/crates/paimon/src/table/blob_resolver.rs
b/crates/paimon/src/table/blob_resolver.rs
index 22cee1b0..e66e4dcc 100644
--- a/crates/paimon/src/table/blob_resolver.rs
+++ b/crates/paimon/src/table/blob_resolver.rs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+use crate::arrow::format::blob::DEFAULT_BLOB_READ_PARALLELISM;
use crate::io::{FileIO, FileRead};
use crate::spec::BlobDescriptor;
use crate::Result;
@@ -29,7 +30,7 @@ use tokio::sync::{OwnedSemaphorePermit, Semaphore};
const BLOB_RANGE_MERGE_GAP: u64 = 64 * 1024;
const BLOB_RANGE_MERGE_MAX_SPAN: u64 = 8 * 1024 * 1024;
-pub(crate) const BLOB_DESCRIPTOR_READ_CONCURRENCY: usize = 8;
+pub(crate) const BLOB_DESCRIPTOR_READ_CONCURRENCY: usize =
DEFAULT_BLOB_READ_PARALLELISM;
const BLOB_DESCRIPTOR_READ_BYTE_UNIT: u64 = 1024 * 1024;
const BLOB_DESCRIPTOR_READ_MAX_IN_FLIGHT_BYTES: u64 = 64 * 1024 * 1024;
@@ -105,6 +106,7 @@ impl BlobReader {
}
let limiter = self.limiter.clone();
+ let blob_parallelism = limiter.parallelism();
let groups: Vec<Vec<(usize, Vec<u8>)>> = stream::iter(by_uri)
.map(|(uri, entries)| {
let limiter = limiter.clone();
@@ -145,7 +147,7 @@ impl BlobReader {
)
}
})
- .buffer_unordered(BLOB_DESCRIPTOR_READ_CONCURRENCY)
+ .buffer_unordered(blob_parallelism)
.try_collect()
.await?;
@@ -322,6 +324,7 @@ pub(crate) struct BlobReadLimiter {
bytes: Arc<Semaphore>,
byte_unit: u64,
max_byte_permits: u32,
+ parallelism: usize,
}
impl BlobReadLimiter {
@@ -333,6 +336,18 @@ impl BlobReadLimiter {
)
}
+ pub(crate) fn with_parallelism(parallelism: usize) -> Self {
+ Self::with_limits(
+ parallelism,
+ BLOB_DESCRIPTOR_READ_MAX_IN_FLIGHT_BYTES,
+ BLOB_DESCRIPTOR_READ_BYTE_UNIT,
+ )
+ }
+
+ pub(crate) fn parallelism(&self) -> usize {
+ self.parallelism
+ }
+
fn with_limits(request_limit: usize, byte_budget: u64, byte_unit: u64) ->
Self {
assert!(request_limit > 0);
assert!(byte_budget > 0);
@@ -344,6 +359,7 @@ impl BlobReadLimiter {
bytes: Arc::new(Semaphore::new(max_byte_permits as usize)),
byte_unit,
max_byte_permits: max_byte_permits as u32,
+ parallelism: request_limit,
}
}
@@ -574,6 +590,7 @@ async fn read_blob_groups(
groups: Vec<BlobReadGroup>,
limiter: BlobReadLimiter,
) -> Result<Vec<ResolvedMergedBlobRead>> {
+ let blob_parallelism = limiter.parallelism();
let grouped_results: Vec<Vec<ResolvedMergedBlobRead>> =
stream::iter(groups)
.map(|group| {
@@ -582,7 +599,7 @@ async fn read_blob_groups(
read_merged_blob_ranges(&group.uri, group.reader,
group.reads, limiter).await
}
})
- .buffer_unordered(BLOB_DESCRIPTOR_READ_CONCURRENCY)
+ .buffer_unordered(blob_parallelism)
.try_collect()
.await?;
Ok(grouped_results.into_iter().flatten().collect())
@@ -594,6 +611,7 @@ async fn read_merged_blob_ranges(
reads: Vec<MergedBlobRead>,
limiter: BlobReadLimiter,
) -> Result<Vec<ResolvedMergedBlobRead>> {
+ let blob_parallelism = limiter.parallelism();
stream::iter(reads)
.map(|merged| {
let uri = uri.to_string();
@@ -627,7 +645,7 @@ async fn read_merged_blob_ranges(
Ok(ResolvedMergedBlobRead { merged, data })
}
})
- .buffer_unordered(BLOB_DESCRIPTOR_READ_CONCURRENCY)
+ .buffer_unordered(blob_parallelism)
.try_collect()
.await
}
@@ -768,6 +786,34 @@ mod tests {
assert!(reader.max_in_flight() <= BLOB_DESCRIPTOR_READ_CONCURRENCY);
}
+ #[tokio::test]
+ async fn test_blob_range_reads_honor_configured_parallelism() {
+ let reader =
TrackingFileRead::new(Bytes::from_static(b"abcdefghijkl"));
+ let reads = (0..12)
+ .map(|row| MergedBlobRead {
+ start: row,
+ end: row + 1,
+ requests: vec![BlobReadRequest {
+ row: row as usize,
+ offset: row,
+ length: 1,
+ }],
+ })
+ .collect();
+
+ let results = read_merged_blob_ranges(
+ "memory:/blob.bin",
+ std::sync::Arc::new(reader.clone()),
+ reads,
+ BlobReadLimiter::with_parallelism(2),
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(results.len(), 12);
+ assert_eq!(reader.max_in_flight(), 2);
+ }
+
#[tokio::test]
async fn test_blob_range_reads_apply_byte_budget_and_preserve_rows() {
let reader = TrackingFileRead::new(Bytes::from_static(b"abcdefgh"));
diff --git a/crates/paimon/src/table/data_evolution_reader.rs
b/crates/paimon/src/table/data_evolution_reader.rs
index a456505a..a4480878 100644
--- a/crates/paimon/src/table/data_evolution_reader.rs
+++ b/crates/paimon/src/table/data_evolution_reader.rs
@@ -17,11 +17,12 @@
mod blob_fallback;
-use super::blob_resolver::{BlobReadLimiter, BLOB_DESCRIPTOR_READ_CONCURRENCY};
+use super::blob_resolver::BlobReadLimiter;
use super::data_file_reader::{
append_null_row_id_column, attach_row_id, expand_selected_row_ids,
insert_column_at,
DataFileReadTiming, DataFileReader,
};
+use crate::arrow::format::blob::DEFAULT_BLOB_READ_PARALLELISM;
use crate::arrow::format::FilePredicates;
use crate::arrow::{build_target_arrow_schema, ParquetReadBudget};
use crate::deletion_vector::{DeletionVector, DeletionVectorFactory};
@@ -112,6 +113,7 @@ pub(crate) struct DataEvolutionReader {
blob_view_resolve_enabled: bool,
blob_view_rest_env: Option<RESTEnv>,
blob_read_limiter: BlobReadLimiter,
+ blob_parallelism: usize,
batch_size: Option<usize>,
parquet_read_budget: Option<Arc<ParquetReadBudget>>,
read_timing: Option<Arc<DataFileReadTiming>>,
@@ -190,6 +192,7 @@ impl DataEvolutionReader {
blob_view_resolve_enabled,
blob_view_rest_env,
blob_read_limiter: BlobReadLimiter::new(),
+ blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
batch_size: None,
parquet_read_budget: None,
read_timing: None,
@@ -201,6 +204,13 @@ impl DataEvolutionReader {
self
}
+ pub(crate) fn with_blob_parallelism(mut self, blob_parallelism: usize) ->
Self {
+ debug_assert!(blob_parallelism > 0);
+ self.blob_read_limiter =
BlobReadLimiter::with_parallelism(blob_parallelism);
+ self.blob_parallelism = blob_parallelism;
+ self
+ }
+
pub(crate) fn with_parquet_read_budget(
mut self,
parquet_read_budget: Option<Arc<ParquetReadBudget>>,
@@ -255,6 +265,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_read_timing(self.read_timing.clone());
@@ -617,6 +628,7 @@ impl DataEvolutionReader {
let table_fields = self.table_fields.clone();
let blob_descriptor_fields = self.blob_descriptor_fields.clone();
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 read_timing = self.read_timing.clone();
@@ -705,6 +717,7 @@ impl DataEvolutionReader {
table_fields.clone(),
batch_size,
blob_as_descriptor,
+ blob_parallelism,
source_parquet_read_budget.clone(),
read_timing.clone(),
anchor_deletion_vector.as_ref(),
@@ -870,17 +883,23 @@ async fn resolve_descriptor_columns(
file_io: &FileIO,
limiter: &BlobReadLimiter,
) -> crate::Result<RecordBatch> {
- resolve_descriptor_columns_with(batch, blob_descriptor_fields, |column| {
+ resolve_descriptor_columns_with(
+ batch,
+ blob_descriptor_fields,
+ limiter.parallelism(),
+ |column| {
let file_io = file_io.clone();
let limiter = limiter.clone();
async move { super::blob_resolver::resolve_blob_column(&column,
&file_io, limiter).await }
- })
+ },
+ )
.await
}
async fn resolve_descriptor_columns_with<F, Fut>(
batch: RecordBatch,
blob_descriptor_fields: &HashSet<String>,
+ blob_parallelism: usize,
resolve: F,
) -> crate::Result<RecordBatch>
where
@@ -913,7 +932,7 @@ where
let future = resolve(column);
async move { future.await.map(|resolved| (idx, resolved)) }
})
- .buffer_unordered(BLOB_DESCRIPTOR_READ_CONCURRENCY)
+ .buffer_unordered(blob_parallelism)
.try_collect()
.await?;
for (idx, resolved) in resolved_columns {
@@ -1237,6 +1256,7 @@ fn open_source_stream(
table_fields: Vec<DataField>,
batch_size: Option<usize>,
blob_as_descriptor: bool,
+ blob_parallelism: usize,
parquet_read_budget: Option<Arc<ParquetReadBudget>>,
read_timing: Option<Arc<DataFileReadTiming>>,
anchor_deletion_vector: Option<&DeletionVectorContext>,
@@ -1272,6 +1292,7 @@ fn open_source_stream(
batch_size,
file_io,
blob_as_descriptor,
+ blob_parallelism,
anchor_deletion_vector.cloned(),
);
}
@@ -1287,6 +1308,7 @@ fn open_source_stream(
batch_size,
file_io,
blob_as_descriptor,
+ blob_parallelism,
anchor_deletion_vector.cloned(),
);
}
@@ -1303,6 +1325,7 @@ fn open_source_stream(
)
.with_batch_size(batch_size)
.with_blob_as_descriptor(blob_as_descriptor)
+ .with_blob_parallelism(blob_parallelism)
.with_parquet_read_budget(parquet_read_budget)
.with_read_timing(read_timing);
@@ -2686,7 +2709,7 @@ mod tests {
let in_flight = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let max_in_flight = Arc::new(std::sync::atomic::AtomicUsize::new(0));
- let resolved = resolve_descriptor_columns_with(batch, &fields,
|column| {
+ let resolved = resolve_descriptor_columns_with(batch, &fields, 2,
|column| {
let in_flight = in_flight.clone();
let max_in_flight = max_in_flight.clone();
async move {
diff --git a/crates/paimon/src/table/data_evolution_reader/blob_fallback.rs
b/crates/paimon/src/table/data_evolution_reader/blob_fallback.rs
index dbd6a0ae..c0674c2c 100644
--- a/crates/paimon/src/table/data_evolution_reader/blob_fallback.rs
+++ b/crates/paimon/src/table/data_evolution_reader/blob_fallback.rs
@@ -51,6 +51,7 @@ impl LazyBlobFile {
positions: &[usize],
file_io: &FileIO,
blob_as_descriptor: bool,
+ blob_parallelism: usize,
field_kind: &BlobFieldKind,
) -> crate::Result<Vec<BlobReadValue>> {
if self.reader.is_none() {
@@ -63,11 +64,12 @@ impl LazyBlobFile {
})?;
let input = file_io.new_input(&self.path)?;
let reader = input.reader().await?;
- let reader = IndexedBlobReader::open(
+ let reader = IndexedBlobReader::open_with_parallelism(
Box::new(reader),
file_size,
self.path.clone(),
blob_as_descriptor,
+ blob_parallelism,
)
.await?;
let indexed_rows =
@@ -116,6 +118,7 @@ pub(super) fn read(
batch_size: Option<usize>,
file_io: FileIO,
blob_as_descriptor: bool,
+ blob_parallelism: usize,
anchor_deletion_vector: Option<DeletionVectorContext>,
) -> crate::Result<ArrowRecordBatchStream> {
if read_fields.len() != 1 ||
!read_fields[0].data_type().is_blob_file_field() {
@@ -180,6 +183,7 @@ pub(super) fn read(
target_schema.clone(),
&file_io,
blob_as_descriptor,
+ blob_parallelism,
&field_kind,
).await?;
}
@@ -193,6 +197,7 @@ async fn resolve_batch(
target_schema: Arc<arrow_schema::Schema>,
file_io: &FileIO,
blob_as_descriptor: bool,
+ blob_parallelism: usize,
field_kind: &BlobFieldKind,
) -> crate::Result<RecordBatch> {
let mut resolved = (0..row_ids.len())
@@ -241,7 +246,13 @@ async fn resolve_batch(
if !file_positions.is_empty() {
let values = file
- .read_positions(&file_positions, file_io,
blob_as_descriptor, field_kind)
+ .read_positions(
+ &file_positions,
+ file_io,
+ blob_as_descriptor,
+ blob_parallelism,
+ field_kind,
+ )
.await?;
for (output_position, value) in
output_positions.into_iter().zip(values) {
if !matches!(&value, BlobReadValue::Placeholder) {
@@ -438,6 +449,7 @@ mod tests {
schema,
&file_io,
false,
+ crate::arrow::format::blob::DEFAULT_BLOB_READ_PARALLELISM,
&BlobFieldKind::Scalar,
)
.await
diff --git a/crates/paimon/src/table/data_file_reader.rs
b/crates/paimon/src/table/data_file_reader.rs
index e5d8e7de..b24f03ad 100644
--- a/crates/paimon/src/table/data_file_reader.rs
+++ b/crates/paimon/src/table/data_file_reader.rs
@@ -16,6 +16,7 @@
// under the License.
use crate::arrow::build_target_arrow_schema;
+use crate::arrow::format::blob::DEFAULT_BLOB_READ_PARALLELISM;
use crate::arrow::format::create_format_reader_with_budget;
use crate::arrow::schema_evolution::{create_index_mapping, NULL_FIELD_INDEX};
use crate::arrow::ParquetReadBudget;
@@ -117,6 +118,7 @@ pub(crate) struct DataFileReader {
file_index_read_enabled: bool,
row_filter_factory: Option<Arc<dyn crate::arrow::RowFilterFactory>>,
blob_as_descriptor: bool,
+ blob_parallelism: usize,
batch_size: Option<usize>,
parquet_read_budget: Option<Arc<ParquetReadBudget>>,
read_timing: Option<Arc<DataFileReadTiming>>,
@@ -141,6 +143,7 @@ impl DataFileReader {
file_index_read_enabled: false,
row_filter_factory: None,
blob_as_descriptor: false,
+ blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
batch_size: None,
parquet_read_budget: None,
read_timing: None,
@@ -152,6 +155,12 @@ impl DataFileReader {
self
}
+ pub(crate) fn with_blob_parallelism(mut self, blob_parallelism: usize) ->
Self {
+ debug_assert!(blob_parallelism > 0);
+ self.blob_parallelism = blob_parallelism;
+ self
+ }
+
pub(crate) fn with_file_index_read_enabled(mut self, enabled: bool) ->
Self {
self.file_index_read_enabled = enabled;
self
@@ -448,6 +457,7 @@ impl DataFileReader {
let file_io = self.file_io.clone();
let split = split.clone();
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 read_timing = self.read_timing.clone();
@@ -509,6 +519,7 @@ impl DataFileReader {
blob_as_descriptor,
&format_read_fields,
parquet_read_budget,
+ blob_parallelism,
)?;
let input_file = file_io.new_input(&path_to_read)?;
let open_start = read_timing.as_ref().map(|_| Instant::now());
@@ -716,6 +727,7 @@ impl DataFileReader {
let file_io = self.file_io.clone();
let split = split.clone();
let blob_as_descriptor = self.blob_as_descriptor;
+ let blob_parallelism = self.blob_parallelism;
let parquet_read_budget = self.parquet_read_budget.clone();
let target_schema = build_target_arrow_schema(&read_type)?;
@@ -781,6 +793,7 @@ impl DataFileReader {
blob_as_descriptor,
&format_read_fields,
parquet_read_budget,
+ blob_parallelism,
)?;
let input_file = file_io.new_input(&path_to_read)?;
let file_reader = input_file.reader().await?;
diff --git a/crates/paimon/src/table/format_read_builder.rs
b/crates/paimon/src/table/format_read_builder.rs
index a2b6f81f..f426e096 100644
--- a/crates/paimon/src/table/format_read_builder.rs
+++ b/crates/paimon/src/table/format_read_builder.rs
@@ -22,6 +22,7 @@ use super::read_builder::split_scan_predicates;
use super::read_builder::{resolve_projected_fields,
validate_projection_possible};
use super::table_read::configured_parquet_read_budget;
use super::{Table, TableRead, TableScan};
+use crate::arrow::format::blob::DEFAULT_BLOB_READ_PARALLELISM;
use crate::arrow::ParquetReadBudget;
use crate::spec::{DataField, Predicate};
use crate::table::source::RowRange;
@@ -42,6 +43,7 @@ pub(crate) struct FormatReadBuilder<'a> {
row_ranges: Option<Vec<RowRange>>,
case_sensitive: bool,
parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ blob_parallelism: usize,
}
impl<'a> FormatReadBuilder<'a> {
@@ -56,6 +58,7 @@ impl<'a> FormatReadBuilder<'a> {
row_ranges: None,
case_sensitive: true,
parquet_read_budget: None,
+ blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
}
}
@@ -115,6 +118,11 @@ impl<'a> FormatReadBuilder<'a> {
self
}
+ pub(crate) fn with_blob_parallelism(&mut self, blob_parallelism: usize) ->
&mut Self {
+ self.blob_parallelism = blob_parallelism;
+ self
+ }
+
pub(crate) fn new_scan(&self) -> TableScan<'a> {
TableScan::new(
self.table,
@@ -144,13 +152,14 @@ impl<'a> FormatReadBuilder<'a> {
Some(budget) => Arc::clone(budget),
None => configured_parquet_read_budget(self.table)?,
};
- Ok(TableRead::new_format(
+ TableRead::new_format(
self.table,
read_type,
self.data_predicates.clone(),
self.limit,
)
- .with_parquet_read_budget(parquet_read_budget))
+ .with_parquet_read_budget(parquet_read_budget)
+ .with_blob_parallelism(self.blob_parallelism)
}
/// Resolve the effective read type, deferring projection name resolution
to
diff --git a/crates/paimon/src/table/format_table_read.rs
b/crates/paimon/src/table/format_table_read.rs
index cb6e232f..c20c3839 100644
--- a/crates/paimon/src/table/format_table_read.rs
+++ b/crates/paimon/src/table/format_table_read.rs
@@ -21,6 +21,7 @@ use super::data_file_reader::DataFileReader;
use super::read_builder::split_scan_predicates;
use super::table_read::configured_parquet_read_budget;
use super::{ArrowRecordBatchStream, Table};
+use crate::arrow::format::blob::DEFAULT_BLOB_READ_PARALLELISM;
use crate::arrow::partition::partition_array;
use crate::arrow::{build_target_arrow_schema, ParquetReadBudget};
use crate::spec::{DataField, Predicate};
@@ -38,6 +39,7 @@ pub(crate) struct FormatTableRead<'a> {
row_filter_factory: Option<Arc<dyn crate::arrow::RowFilterFactory>>,
parquet_read_budget: Option<Arc<ParquetReadBudget>>,
limit: Option<usize>,
+ blob_parallelism: usize,
}
impl<'a> FormatTableRead<'a> {
@@ -54,6 +56,7 @@ impl<'a> FormatTableRead<'a> {
row_filter_factory: None,
parquet_read_budget: None,
limit,
+ blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
}
}
@@ -87,6 +90,11 @@ impl<'a> FormatTableRead<'a> {
self
}
+ pub(crate) fn with_blob_parallelism(mut self, blob_parallelism: usize) ->
Self {
+ self.blob_parallelism = blob_parallelism;
+ self
+ }
+
fn parquet_read_budget(&self) -> crate::Result<Arc<ParquetReadBudget>> {
match &self.parquet_read_budget {
Some(budget) => Ok(Arc::clone(budget)),
@@ -121,6 +129,7 @@ impl<'a> FormatTableRead<'a> {
let batch_size = Some(core_options.read_batch_size()?);
let row_filter_factory = self.row_filter_factory.clone();
let parquet_read_budget = Some(self.parquet_read_budget()?);
+ let blob_parallelism = self.blob_parallelism;
Ok(try_stream! {
for split in splits {
@@ -137,6 +146,7 @@ impl<'a> FormatTableRead<'a> {
data_predicates.clone(),
)
.with_batch_size(batch_size)
+ .with_blob_parallelism(blob_parallelism)
.with_parquet_read_budget(parquet_read_budget.clone());
if let Some(factory) = &row_filter_factory {
reader =
reader.with_row_filter_factory(Arc::clone(factory));
diff --git a/crates/paimon/src/table/read_builder.rs
b/crates/paimon/src/table/read_builder.rs
index c258a1c5..f5087883 100644
--- a/crates/paimon/src/table/read_builder.rs
+++ b/crates/paimon/src/table/read_builder.rs
@@ -26,6 +26,7 @@ use super::incremental_scan::{IncrementalScan,
IncrementalScanMode};
use super::partition_filter::PartitionFilter;
use super::table_read::{configured_parquet_read_budget, TableRead};
use super::{Table, TableScan};
+use crate::arrow::format::blob::DEFAULT_BLOB_READ_PARALLELISM;
use crate::spec::{CoreOptions, DataField, Predicate};
use crate::table::source::RowRange;
use crate::{Error, Result};
@@ -237,6 +238,25 @@ impl<'a> ReadBuilder<'a> {
self
}
+ /// Set the maximum number of concurrent BLOB range reads for the
resulting read.
+ pub fn with_blob_parallelism(&mut self, blob_parallelism: usize) ->
Result<&mut Self> {
+ if blob_parallelism == 0 {
+ return Err(Error::DataInvalid {
+ message: "BLOB read parallelism must be greater than
zero".to_string(),
+ source: None,
+ });
+ }
+ match &mut self.0 {
+ ReadBuilderKind::Paimon(builder) => {
+ builder.with_blob_parallelism(blob_parallelism);
+ }
+ ReadBuilderKind::Format(builder) => {
+ builder.with_blob_parallelism(blob_parallelism);
+ }
+ }
+ Ok(self)
+ }
+
/// Inject a Parquet budget shared with sibling scan partitions.
#[doc(hidden)]
pub fn with_parquet_read_budget(
@@ -317,6 +337,7 @@ struct PaimonReadBuilder<'a> {
derived_row_ranges: Option<Vec<RowRange>>,
case_sensitive: bool,
parquet_read_budget: Option<Arc<crate::arrow::ParquetReadBudget>>,
+ blob_parallelism: usize,
}
impl<'a> PaimonReadBuilder<'a> {
@@ -331,6 +352,7 @@ impl<'a> PaimonReadBuilder<'a> {
derived_row_ranges: None,
case_sensitive: true,
parquet_read_budget: None,
+ blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
}
}
@@ -471,6 +493,11 @@ impl<'a> PaimonReadBuilder<'a> {
self
}
+ fn with_blob_parallelism(&mut self, blob_parallelism: usize) -> &mut Self {
+ self.blob_parallelism = blob_parallelism;
+ self
+ }
+
/// Create a table scan. Call [TableScan::plan] to get splits.
///
/// Projection names are resolved here on a best-effort basis: the resolved
@@ -519,10 +546,9 @@ impl<'a> PaimonReadBuilder<'a> {
Some(budget) => Arc::clone(budget),
None => configured_parquet_read_budget(self.table)?,
};
- Ok(
- TableRead::new(self.table, read_type,
self.filter.data_predicates.clone())
- .with_parquet_read_budget(parquet_read_budget),
- )
+ TableRead::new(self.table, read_type,
self.filter.data_predicates.clone())
+ .with_parquet_read_budget(parquet_read_budget)
+ .with_blob_parallelism(self.blob_parallelism)
}
/// Resolve the effective read type, deferring projection name resolution
to
diff --git a/crates/paimon/src/table/table_read.rs
b/crates/paimon/src/table/table_read.rs
index b684d411..9d641841 100644
--- a/crates/paimon/src/table/table_read.rs
+++ b/crates/paimon/src/table/table_read.rs
@@ -23,6 +23,7 @@ use super::kv_file_reader::{KeyValueFileReader,
KeyValueReadConfig};
use super::read_builder::split_scan_predicates;
use super::{ArrowRecordBatchStream, Table};
use crate::arrow::build_target_arrow_schema;
+use crate::arrow::format::blob::DEFAULT_BLOB_READ_PARALLELISM;
use crate::arrow::ParquetReadBudget;
use crate::spec::{
BigIntType, CoreOptions, DataField, DataType, MergeEngine, Predicate,
TinyIntType,
@@ -133,6 +134,24 @@ impl<'a> TableRead<'a> {
}
}
+ /// Set the maximum number of concurrent BLOB range reads for this read.
+ pub fn with_blob_parallelism(self, blob_parallelism: usize) ->
crate::Result<Self> {
+ if blob_parallelism == 0 {
+ return Err(crate::Error::DataInvalid {
+ message: "BLOB read parallelism must be greater than
zero".to_string(),
+ source: None,
+ });
+ }
+ Ok(match self.0 {
+ TableReadKind::Paimon(read) => Self(TableReadKind::Paimon(
+ read.with_blob_parallelism(blob_parallelism),
+ )),
+ TableReadKind::Format(read) => Self(TableReadKind::Format(
+ read.with_blob_parallelism(blob_parallelism),
+ )),
+ })
+ }
+
/// Attach an engine-specific Parquet decoder-filter factory.
///
/// The hook is used only by schema-identical raw reads. Callers must still
@@ -230,6 +249,7 @@ struct PaimonTableRead<'a> {
row_filter_factory: Option<Arc<dyn crate::arrow::RowFilterFactory>>,
parquet_read_budget: Option<Arc<ParquetReadBudget>>,
data_file_read_timing: Option<Arc<DataFileReadTiming>>,
+ blob_parallelism: usize,
}
impl<'a> PaimonTableRead<'a> {
@@ -246,6 +266,7 @@ impl<'a> PaimonTableRead<'a> {
row_filter_factory: None,
parquet_read_budget: None,
data_file_read_timing: None,
+ blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
}
}
@@ -294,6 +315,11 @@ impl<'a> PaimonTableRead<'a> {
self
}
+ fn with_blob_parallelism(mut self, blob_parallelism: usize) -> Self {
+ self.blob_parallelism = blob_parallelism;
+ self
+ }
+
fn parquet_read_budget(&self) -> crate::Result<Arc<ParquetReadBudget>> {
match &self.parquet_read_budget {
Some(budget) => Ok(Arc::clone(budget)),
@@ -337,6 +363,7 @@ impl<'a> PaimonTableRead<'a> {
let read_type = self.read_type.clone();
let data_predicates = self.data_predicates.clone();
let parquet_read_budget = self.parquet_read_budget()?;
+ let blob_parallelism = self.blob_parallelism;
Ok(Box::pin(async_stream::try_stream! {
let mut workers = stream::iter(pairs.into_iter().map(|(before,
after)| {
@@ -346,7 +373,8 @@ impl<'a> PaimonTableRead<'a> {
let parquet_read_budget = Arc::clone(&parquet_read_budget);
let worker: ArrowRecordBatchStream =
Box::pin(async_stream::try_stream! {
let pair_read = PaimonTableRead::new(&table, read_type,
data_predicates)
- .with_parquet_read_budget(parquet_read_budget);
+ .with_parquet_read_budget(parquet_read_budget)
+ .with_blob_parallelism(blob_parallelism);
let mut pair_stream =
pair_read.to_diff_after_image_stream(&before, &after)?;
while let Some(batch) = pair_stream.next().await {
yield batch?;
@@ -421,6 +449,7 @@ impl<'a> PaimonTableRead<'a> {
)
.with_file_index_read_enabled(core_options.file_index_read_enabled())
.with_batch_size(Some(core_options.read_batch_size()?))
+ .with_blob_parallelism(self.blob_parallelism)
.with_parquet_read_budget(Some(self.parquet_read_budget()?));
let raw_stream = reader.read(&data_splits)?;
@@ -480,6 +509,7 @@ impl<'a> PaimonTableRead<'a> {
let read_type = self.read_type.clone();
let data_predicates = self.data_predicates.clone();
let parquet_read_budget = self.parquet_read_budget()?;
+ let blob_parallelism = self.blob_parallelism;
Ok(Box::pin(async_stream::try_stream! {
let mut workers = stream::iter(pairs.into_iter().map(|(before,
after)| {
@@ -489,7 +519,8 @@ impl<'a> PaimonTableRead<'a> {
let parquet_read_budget = Arc::clone(&parquet_read_budget);
let worker: ArrowRecordBatchStream =
Box::pin(async_stream::try_stream! {
let pair_read = PaimonTableRead::new(&table, read_type,
data_predicates)
- .with_parquet_read_budget(parquet_read_budget);
+ .with_parquet_read_budget(parquet_read_budget)
+ .with_blob_parallelism(blob_parallelism);
let mut pair_stream =
pair_read.to_audit_log_arrow_for_diff(&before,
&after)?;
while let Some(batch) = pair_stream.next().await {
@@ -535,11 +566,13 @@ impl<'a> PaimonTableRead<'a> {
let read_type_for_output = self.read_type.clone();
let data_predicates = self.data_predicates.clone();
let parquet_read_budget = self.parquet_read_budget()?;
+ let blob_parallelism = self.blob_parallelism;
Ok(Box::pin(async_stream::try_stream! {
let core_options = CoreOptions::new(table.schema().options());
let pair_read = PaimonTableRead::new(&table,
diff_read_type.clone(), data_predicates)
- .with_parquet_read_budget(parquet_read_budget);
+ .with_parquet_read_budget(parquet_read_budget)
+ .with_blob_parallelism(blob_parallelism);
let before_stream =
pair_read.read_pk_sorted_for_diff_with_type(&before,
&core_options, &diff_read_type)?;
let after_stream =
@@ -621,11 +654,13 @@ impl<'a> PaimonTableRead<'a> {
let before = before.to_vec();
let after = after.to_vec();
let parquet_read_budget = self.parquet_read_budget()?;
+ let blob_parallelism = self.blob_parallelism;
Ok(Box::pin(async_stream::try_stream! {
let core_options = CoreOptions::new(table.schema().options());
let pair_read = PaimonTableRead::new(&table,
diff_read_type.clone(), data_predicates)
- .with_parquet_read_budget(parquet_read_budget);
+ .with_parquet_read_budget(parquet_read_budget)
+ .with_blob_parallelism(blob_parallelism);
let before_stream = pair_read.read_pk_sorted_for_diff_with_type(
&before,
&core_options,
@@ -894,6 +929,7 @@ impl<'a> PaimonTableRead<'a> {
self.table.rest_env().cloned(),
)?
.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_read_timing(self.data_file_read_timing.clone());
reader.read(data_splits)
@@ -916,6 +952,7 @@ impl<'a> PaimonTableRead<'a> {
)
.with_file_index_read_enabled(core_options.file_index_read_enabled())
.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_read_timing(self.data_file_read_timing.clone());
// The engine decoder filter is safe only on the plain append/raw path.