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 4178002a feat(mosaic): read row-group prefetch depth and byte budget
from table options (#864)
4178002a is described below
commit 4178002ae77460a1f0518724e86c37eeb93ee6de
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Fri Sep 18 22:31:02 2026 +0800
feat(mosaic): read row-group prefetch depth and byte budget from table
options (#864)
---
.../datafusion/src/physical_plan/scan.rs | 10 +-
crates/paimon/src/arrow/format/mod.rs | 12 +-
crates/paimon/src/arrow/format/mosaic.rs | 238 +++++++++------------
crates/paimon/src/arrow/format/parquet.rs | 18 +-
crates/paimon/src/arrow/mod.rs | 4 +-
.../{parquet_read_budget.rs => read_budget.rs} | 205 +++++++++++++-----
crates/paimon/src/spec/core_options.rs | 88 +++++++-
crates/paimon/src/table/audit_log_table/read.rs | 2 +
crates/paimon/src/table/data_evolution_reader.rs | 24 ++-
crates/paimon/src/table/data_file_reader.rs | 19 +-
crates/paimon/src/table/format_read_builder.rs | 6 +-
crates/paimon/src/table/format_table_read.rs | 14 +-
crates/paimon/src/table/kv_file_reader.rs | 28 ++-
crates/paimon/src/table/read_builder.rs | 12 +-
crates/paimon/src/table/table_read.rs | 72 ++++++-
15 files changed, 496 insertions(+), 256 deletions(-)
diff --git a/crates/integrations/datafusion/src/physical_plan/scan.rs
b/crates/integrations/datafusion/src/physical_plan/scan.rs
index 432e3a2a..01a9909f 100644
--- a/crates/integrations/datafusion/src/physical_plan/scan.rs
+++ b/crates/integrations/datafusion/src/physical_plan/scan.rs
@@ -49,7 +49,7 @@ use datafusion::physical_plan::filter_pushdown::{
use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
use datafusion::physical_plan::{DisplayAs, ExecutionPlan, Partitioning,
PlanProperties};
use futures::{FutureExt, StreamExt, TryStreamExt};
-use paimon::arrow::ParquetReadBudget;
+use paimon::arrow::ReadBudget;
use paimon::spec::{
CoreOptions, DataField, Datum, MergeEngine, Predicate, PredicateBuilder,
PredicateOperator,
};
@@ -804,7 +804,7 @@ pub struct PaimonTableScan {
/// Parquet row filter instead, avoiding duplicate decoder evaluation.
decoder_filters: Vec<Arc<dyn PhysicalExpr>>,
/// Query-wide budget shared by every DataFusion scan partition.
- parquet_read_budget: Arc<ParquetReadBudget>,
+ parquet_read_budget: Arc<ReadBudget>,
}
impl PaimonTableScan {
@@ -833,7 +833,7 @@ impl PaimonTableScan {
scan_trace,
pushed_variants,
case_sensitive,
- Arc::new(ParquetReadBudget::default()),
+ Arc::new(ReadBudget::default()),
)
}
@@ -852,7 +852,7 @@ impl PaimonTableScan {
) -> DFResult<Self> {
let options = table.schema().core_options();
let parquet_read_budget = Arc::new(
- ParquetReadBudget::new(
+ ReadBudget::new(
options
.parquet_row_group_parallelism()
.map_err(to_datafusion_error)?,
@@ -889,7 +889,7 @@ impl PaimonTableScan {
scan_trace: Option<ScanTrace>,
pushed_variants: Option<String>,
case_sensitive: bool,
- parquet_read_budget: Arc<ParquetReadBudget>,
+ parquet_read_budget: Arc<ReadBudget>,
) -> Self {
let plan_properties = Arc::new(PlanProperties::new(
EquivalenceProperties::new(schema.clone()),
diff --git a/crates/paimon/src/arrow/format/mod.rs
b/crates/paimon/src/arrow/format/mod.rs
index 17d21c9f..4fc9f93b 100644
--- a/crates/paimon/src/arrow/format/mod.rs
+++ b/crates/paimon/src/arrow/format/mod.rs
@@ -25,10 +25,11 @@ mod shredding;
#[cfg(feature = "vortex")]
mod vortex;
+pub(crate) use mosaic::MosaicPrefetchOptions;
#[cfg(test)]
pub(crate) use parquet::ParquetFormatWriter;
-use super::ParquetReadBudget;
+use super::ReadBudget;
use super::RowFilterFactory;
use crate::io::{FileIO, FileRead, OutputFile};
use crate::spec::stats::BinaryTableStats;
@@ -196,16 +197,19 @@ pub(crate) fn create_format_reader(
read_fields,
None,
blob::DEFAULT_BLOB_READ_PARALLELISM,
+ MosaicPrefetchOptions::default(),
)
}
-/// Create a format reader with a scan-shared Parquet resource budget.
+/// Create a format reader with a scan-shared Parquet resource budget and the
+/// per-file Mosaic prefetch settings.
pub(crate) fn create_format_reader_with_budget(
path: &str,
blob_as_descriptor: bool,
read_fields: &[DataField],
- parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ 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") {
@@ -227,7 +231,7 @@ pub(crate) fn create_format_reader_with_budget(
} else {
if lower.ends_with(".mosaic") {
return Ok(shredding::maybe_wrap_reader(
- Box::new(mosaic::MosaicFormatReader),
+
Box::new(mosaic::MosaicFormatReader::with_prefetch(mosaic_prefetch)),
read_fields,
));
}
diff --git a/crates/paimon/src/arrow/format/mosaic.rs
b/crates/paimon/src/arrow/format/mosaic.rs
index ae7c5011..fa0d8f93 100644
--- a/crates/paimon/src/arrow/format/mosaic.rs
+++ b/crates/paimon/src/arrow/format/mosaic.rs
@@ -20,8 +20,10 @@ use crate::arrow::build_target_arrow_schema;
use crate::arrow::filtering::{
predicates_may_match_with_schema, remap_predicates_to_file, StatsAccessor,
};
+use crate::arrow::read_budget::{ReadBudget, ReadPermit};
use crate::arrow::residual::{filter_record_batch_by_predicates,
widen_scan_fields};
use crate::io::FileRead;
+use crate::spec::MOSAIC_READ_PREFETCH_MAX_BYTES_OPTION;
use crate::spec::{DataField, DataType as PaimonDataType, Datum, Predicate};
use crate::table::{ArrowRecordBatchStream, RowRange};
use crate::Error;
@@ -41,9 +43,37 @@ use std::collections::{HashMap, HashSet, VecDeque};
use std::io;
use std::ops::Range;
use std::panic::{catch_unwind, AssertUnwindSafe};
-use std::sync::{Arc, Condvar, Mutex, OnceLock};
+use std::sync::{Arc, Mutex, OnceLock};
+
+/// Row-group prefetch settings of one Mosaic file read, from the table options
+/// `mosaic.read.prefetch-row-groups` and `mosaic.read.prefetch-max-bytes`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) struct MosaicPrefetchOptions {
+ /// Row groups kept opening ahead of the consumer; `0` reads on demand.
+ pub row_groups: usize,
+ /// Estimated decoded bytes allowed in flight beyond the head row group.
+ pub max_bytes: usize,
+}
+
+impl Default for MosaicPrefetchOptions {
+ fn default() -> Self {
+ Self {
+ row_groups: DEFAULT_PREFETCH_ROW_GROUPS,
+ max_bytes: DEFAULT_PREFETCH_MAX_BYTES,
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct MosaicFormatReader {
+ prefetch: MosaicPrefetchOptions,
+}
-pub(crate) struct MosaicFormatReader;
+impl MosaicFormatReader {
+ pub(crate) fn with_prefetch(prefetch: MosaicPrefetchOptions) -> Self {
+ Self { prefetch }
+ }
+}
const DEFAULT_BATCH_SIZE: usize = 8192;
const DEFAULT_PREFETCH_ROW_GROUPS: usize = 8;
@@ -77,6 +107,7 @@ impl FormatFileReader for MosaicFormatReader {
file_fields: predicates.file_fields.clone(),
});
let batch_size = batch_size.unwrap_or(DEFAULT_BATCH_SIZE);
+ let prefetch = self.prefetch;
let (mut batch_tx, mut batch_rx) = futures::channel::mpsc::channel(1);
let read_task = tokio::task::spawn_blocking(move || {
let result = read_mosaic_batches_blocking(
@@ -88,8 +119,8 @@ impl FormatFileReader for MosaicFormatReader {
batch_size,
row_selection,
handle,
- prefetch_row_groups: DEFAULT_PREFETCH_ROW_GROUPS,
- prefetch_max_bytes: DEFAULT_PREFETCH_MAX_BYTES,
+ prefetch_row_groups: prefetch.row_groups,
+ prefetch_max_bytes: prefetch.max_bytes,
},
|batch|
futures::executor::block_on(batch_tx.send(Ok(batch))).is_ok(),
);
@@ -135,112 +166,13 @@ struct PlannedRowGroup {
}
struct PendingRowGroup {
- permit: MosaicPrefetchPermit,
+ permit: ReadPermit,
task: MosaicTask<RecordBatch>,
}
struct BudgetedBatch {
batch: RecordBatch,
- permit: Option<Arc<MosaicPrefetchPermit>>,
-}
-
-#[derive(Default)]
-struct MosaicPrefetchBudgetState {
- bytes: usize,
- groups: usize,
-}
-
-struct MosaicPrefetchBudgetInner {
- max_bytes: usize,
- max_groups: usize,
- state: Mutex<MosaicPrefetchBudgetState>,
- released: Condvar,
-}
-
-#[derive(Clone)]
-struct MosaicPrefetchBudget {
- inner: Arc<MosaicPrefetchBudgetInner>,
-}
-
-impl MosaicPrefetchBudget {
- fn new(max_bytes: usize, max_groups: usize) -> Self {
- Self {
- inner: Arc::new(MosaicPrefetchBudgetInner {
- max_bytes,
- max_groups,
- state: Mutex::new(MosaicPrefetchBudgetState::default()),
- released: Condvar::new(),
- }),
- }
- }
-
- fn try_acquire(&self, estimated_bytes: usize) ->
Option<MosaicPrefetchPermit> {
- let mut state = self
- .inner
- .state
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- if !self.can_acquire(&state, estimated_bytes) {
- return None;
- }
- Self::charge(&mut state, estimated_bytes);
- Some(MosaicPrefetchPermit {
- inner: Arc::clone(&self.inner),
- estimated_bytes,
- })
- }
-
- fn acquire(&self, estimated_bytes: usize) -> MosaicPrefetchPermit {
- let mut state = self
- .inner
- .state
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- while !self.can_acquire(&state, estimated_bytes) {
- state = self
- .inner
- .released
- .wait(state)
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- }
- Self::charge(&mut state, estimated_bytes);
- MosaicPrefetchPermit {
- inner: Arc::clone(&self.inner),
- estimated_bytes,
- }
- }
-
- fn can_acquire(&self, state: &MosaicPrefetchBudgetState, estimated_bytes:
usize) -> bool {
- if state.groups >= self.inner.max_groups {
- return false;
- }
- // An oversized head group may consume the whole budget so the reader
- // always makes progress, but it cannot overlap peers.
- state.groups == 0 || state.bytes.saturating_add(estimated_bytes) <=
self.inner.max_bytes
- }
-
- fn charge(state: &mut MosaicPrefetchBudgetState, estimated_bytes: usize) {
- state.groups += 1;
- state.bytes = state.bytes.saturating_add(estimated_bytes);
- }
-}
-
-struct MosaicPrefetchPermit {
- inner: Arc<MosaicPrefetchBudgetInner>,
- estimated_bytes: usize,
-}
-
-impl Drop for MosaicPrefetchPermit {
- fn drop(&mut self) {
- let mut state = self
- .inner
- .state
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- state.groups -= 1;
- state.bytes = state.bytes.saturating_sub(self.estimated_bytes);
- self.inner.released.notify_all();
- }
+ permit: Option<Arc<ReadPermit>>,
}
type MosaicJob = Box<dyn FnOnce() + Send + 'static>;
@@ -476,12 +408,14 @@ fn read_mosaic_batches_blocking(
let projected_names = Arc::new(projected_names);
let executor = mosaic_executor()?;
let mut pending = VecDeque::<PendingRowGroup>::new();
- let prefetch_budget = MosaicPrefetchBudget::new(prefetch_max_bytes,
prefetch_row_groups);
+ let prefetch_budget =
+ ReadBudget::with_byte_granularity(prefetch_row_groups,
prefetch_max_bytes as u64, 1)?
+ .with_byte_option(MOSAIC_READ_PREFETCH_MAX_BYTES_OPTION);
let mut outcome = Ok(());
'read: while !planned.is_empty() || !pending.is_empty() {
while let Some(next) = planned.front() {
- let Some(permit) =
prefetch_budget.try_acquire(next.estimated_bytes) else {
+ let Some(permit) =
prefetch_budget.try_acquire(next.estimated_bytes as u64) else {
break;
};
let plan = planned.pop_front().unwrap();
@@ -506,7 +440,13 @@ fn read_mosaic_batches_blocking(
};
// Earlier decoded batches can still own the budget downstream.
// With no task left to join, wait for their permits to be
released.
- let permit = prefetch_budget.acquire(next.estimated_bytes);
+ let permit = match
prefetch_budget.acquire_blocking(next.estimated_bytes as u64) {
+ Ok(permit) => permit,
+ Err(error) => {
+ outcome = Err(error);
+ break 'read;
+ }
+ };
let plan = planned.pop_front().unwrap();
let reader = Arc::clone(&mosaic_reader);
let names = Arc::clone(&projected_names);
@@ -600,7 +540,7 @@ fn apply_mosaic_residual(
fn send_mosaic_batch_chunks(
batch: RecordBatch,
batch_size: usize,
- permit: Option<Arc<MosaicPrefetchPermit>>,
+ permit: Option<Arc<ReadPermit>>,
send_batch: &mut impl FnMut(BudgetedBatch) -> bool,
) -> bool {
split_batch(batch, batch_size).into_iter().all(|batch| {
@@ -1308,7 +1248,7 @@ mod tests {
row_selection: Option<Vec<RowRange>>,
) -> crate::Result<Vec<RecordBatch>> {
let file_size = data.len() as u64;
- MosaicFormatReader
+ MosaicFormatReader::default()
.read_batch_stream(
Box::new(TestFileRead { data }),
file_size,
@@ -1330,7 +1270,7 @@ mod tests {
) -> crate::Result<Vec<Range<u64>>> {
let file_size = data.len() as u64;
let calls = Arc::new(Mutex::new(Vec::new()));
- let _: Vec<RecordBatch> = MosaicFormatReader
+ let _: Vec<RecordBatch> = MosaicFormatReader::default()
.read_batch_stream(
Box::new(TrackingFileRead {
data,
@@ -1356,7 +1296,7 @@ mod tests {
) -> crate::Result<Vec<Range<u64>>> {
let file_size = data.len() as u64;
let calls = Arc::new(Mutex::new(Vec::new()));
- let _: Vec<RecordBatch> = MosaicFormatReader
+ let _: Vec<RecordBatch> = MosaicFormatReader::default()
.read_batch_stream(
Box::new(TrackingFileRead {
data,
@@ -1525,6 +1465,55 @@ mod tests {
assert_eq!(ids.value(4), 5);
}
+ async fn read_via_format_reader_tracking(
+ data: Bytes,
+ fields: Vec<DataField>,
+ prefetch: MosaicPrefetchOptions,
+ ) -> (Vec<RecordBatch>, usize) {
+ let max_active = Arc::new(AtomicUsize::new(0));
+ let file_size = data.len() as u64;
+ let read = ConcurrentTrackingFileRead {
+ data,
+ active: Arc::new(AtomicUsize::new(0)),
+ max_active: Arc::clone(&max_active),
+ };
+ let stream = MosaicFormatReader::with_prefetch(prefetch)
+ .read_batch_stream(Box::new(read), file_size, &fields, None, None,
None)
+ .await
+ .unwrap();
+ let batches = stream.try_collect::<Vec<_>>().await.unwrap();
+ (batches, max_active.load(Ordering::SeqCst))
+ }
+
+ #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+ async fn test_format_reader_prefetch_options_control_look_ahead() {
+ let fields = vec![data_fields()[0].clone()];
+
+ let (batches, max_active) = read_via_format_reader_tracking(
+ multi_row_group_mosaic(Vec::new()),
+ fields.clone(),
+ MosaicPrefetchOptions {
+ row_groups: 0,
+ max_bytes: usize::MAX,
+ },
+ )
+ .await;
+ assert_eq!(collect_i32_column(&batches, 0), vec![1, 2, 10, 11, 20,
21]);
+ assert_eq!(max_active, 1, "depth 0 must read row groups on demand");
+
+ let (batches, max_active) = read_via_format_reader_tracking(
+ multi_row_group_mosaic(Vec::new()),
+ fields,
+ MosaicPrefetchOptions::default(),
+ )
+ .await;
+ assert_eq!(collect_i32_column(&batches, 0), vec![1, 2, 10, 11, 20,
21]);
+ assert!(
+ max_active >= 2,
+ "default prefetch should overlap storage reads,
max_active={max_active}"
+ );
+ }
+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_prefetch_overlaps_row_group_reads_and_preserves_order() {
let data = multi_row_group_mosaic(Vec::new());
@@ -1574,7 +1563,7 @@ mod tests {
}
#[test]
- fn
test_mosaic_prefetch_budget_is_cumulative_and_oversized_head_progresses() {
+ fn test_estimated_row_bytes_matches_java_prefetch_budget() {
assert_eq!(estimated_row_bytes(&[data_fields()[0].clone()]), 5);
assert_eq!(estimated_row_bytes(&[data_fields()[1].clone()]), 40);
assert_eq!(
@@ -1585,25 +1574,6 @@ mod tests {
)]),
128
);
-
- let budget = MosaicPrefetchBudget::new(20, 8);
- let first = budget.try_acquire(10).unwrap();
- let second = budget.try_acquire(10).unwrap();
- assert!(budget.try_acquire(10).is_none());
- drop(first);
- let third = budget.try_acquire(10).unwrap();
- drop((second, third));
-
- let oversized = budget.try_acquire(21).unwrap();
- assert!(budget.try_acquire(1).is_none());
- drop(oversized);
- assert!(budget.try_acquire(1).is_some());
-
- let group_limited = MosaicPrefetchBudget::new(usize::MAX, 2);
- let first = group_limited.try_acquire(1).unwrap();
- let second = group_limited.try_acquire(1).unwrap();
- assert!(group_limited.try_acquire(1).is_none());
- drop((first, second));
}
#[tokio::test]
@@ -1656,7 +1626,7 @@ mod tests {
let calls = Arc::new(Mutex::new(Vec::new()));
assert!(file_size > 64 * 1024);
- let batches = MosaicFormatReader
+ let batches = MosaicFormatReader::default()
.read_batch_stream(
Box::new(TrackingFileRead {
data,
@@ -1712,7 +1682,7 @@ mod tests {
.unwrap();
let batches = runtime.block_on(async move {
- MosaicFormatReader
+ MosaicFormatReader::default()
.read_batch_stream(
Box::new(RuntimeThreadFileRead {
data,
diff --git a/crates/paimon/src/arrow/format/parquet.rs
b/crates/paimon/src/arrow/format/parquet.rs
index 7b5ec592..758fb36e 100644
--- a/crates/paimon/src/arrow/format/parquet.rs
+++ b/crates/paimon/src/arrow/format/parquet.rs
@@ -18,10 +18,10 @@
use super::shredding::PhysicalFormatWriterFactory;
use super::{FilePredicates, FormatFileReader, FormatFileWriter,
FormatWriteResult};
use crate::arrow::filtering::{predicates_may_match_with_schema, StatsAccessor};
-use crate::arrow::parquet_read_budget::ParquetReadPermit;
+use crate::arrow::read_budget::ReadPermit;
use crate::arrow::shredding::map::MapShreddingReadPlan;
use crate::arrow::shredding::ShreddingReadPlan;
-use crate::arrow::{ParquetReadBudget, RowFilter, RowFilterContext};
+use crate::arrow::{ReadBudget, RowFilter, RowFilterContext};
use crate::io::{FileRead, OutputFile};
use crate::spec::stats::BinaryTableStats;
use crate::spec::{
@@ -56,11 +56,11 @@ use tokio::sync::mpsc;
#[derive(Default)]
pub(crate) struct ParquetFormatReader {
- read_budget: Option<Arc<ParquetReadBudget>>,
+ read_budget: Option<Arc<ReadBudget>>,
}
impl ParquetFormatReader {
- pub(crate) fn with_read_budget(read_budget: Arc<ParquetReadBudget>) ->
Self {
+ pub(crate) fn with_read_budget(read_budget: Arc<ReadBudget>) -> Self {
Self {
read_budget: Some(read_budget),
}
@@ -702,7 +702,7 @@ async fn read_row_group(
projection: ProjectionMask,
row_group_index: usize,
batch_size: Option<usize>,
- _permit: ParquetReadPermit,
+ _permit: ReadPermit,
sender: mpsc::Sender<ParquetRowGroupMessage>,
) {
let mut builder = ParquetRecordBatchStreamBuilder::new_with_metadata(
@@ -2300,7 +2300,7 @@ mod tests {
use crate::arrow::format::{
create_format_reader, create_format_writer, FormatFileReader,
FormatFileWriter,
};
- use crate::arrow::{build_target_arrow_schema, variant_arrow_type,
ParquetReadBudget};
+ use crate::arrow::{build_target_arrow_schema, variant_arrow_type,
ReadBudget};
use crate::io::FileIOBuilder;
use crate::spec::{
ArrayType, BigIntType, DataField, DataType, Datum, IntType, MapType,
PredicateBuilder,
@@ -2838,7 +2838,7 @@ mod tests {
DataType::Int(IntType::new()),
)];
let batches = ParquetFormatReader::with_read_budget(Arc::new(
- ParquetReadBudget::new(8, 256 * 1024 * 1024).unwrap(),
+ ReadBudget::new(8, 256 * 1024 * 1024).unwrap(),
))
.read_batch_stream(
Box::new(file_reader),
@@ -2910,7 +2910,7 @@ mod tests {
"id".to_string(),
DataType::Int(IntType::new()),
)];
- let budget = Arc::new(ParquetReadBudget::new(2, 256 * 1024 *
1024).unwrap());
+ let budget = Arc::new(ReadBudget::new(2, 256 * 1024 * 1024).unwrap());
let first = ParquetFormatReader::with_read_budget(Arc::clone(&budget))
.read_batch_stream(
Box::new(new_file_reader()),
@@ -2965,7 +2965,7 @@ mod tests {
#[tokio::test]
async fn test_parquet_diagnostics_include_reads_with_row_selection() {
let data = write_multi_row_group_parquet(32, 64,
EnabledStatistics::Chunk).await;
- let budget = Arc::new(ParquetReadBudget::new(8, 256 * 1024 *
1024).unwrap());
+ let budget = Arc::new(ReadBudget::new(8, 256 * 1024 * 1024).unwrap());
budget.enable_diagnostics();
let file_size = data.len() as u64;
let fields = vec![int_field("id")];
diff --git a/crates/paimon/src/arrow/mod.rs b/crates/paimon/src/arrow/mod.rs
index 1f017aee..4792cdfb 100644
--- a/crates/paimon/src/arrow/mod.rs
+++ b/crates/paimon/src/arrow/mod.rs
@@ -18,14 +18,14 @@
pub(crate) mod filtering;
pub(crate) mod format;
pub(crate) mod nested_evolution;
-mod parquet_read_budget;
pub(crate) mod partition;
+mod read_budget;
pub(crate) mod residual;
mod row_filter;
pub(crate) mod schema_evolution;
pub(crate) mod shredding;
-pub use parquet_read_budget::ParquetReadBudget;
+pub use read_budget::ReadBudget;
pub use row_filter::{RowFilter, RowFilterContext, RowFilterFactory};
use crate::spec::{
diff --git a/crates/paimon/src/arrow/parquet_read_budget.rs
b/crates/paimon/src/arrow/read_budget.rs
similarity index 58%
rename from crates/paimon/src/arrow/parquet_read_budget.rs
rename to crates/paimon/src/arrow/read_budget.rs
index bef9c6eb..334ac8e3 100644
--- a/crates/paimon/src/arrow/parquet_read_budget.rs
+++ b/crates/paimon/src/arrow/read_budget.rs
@@ -20,24 +20,31 @@ use std::sync::Arc;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
+use crate::spec::PARQUET_ROW_GROUP_MAX_INFLIGHT_BYTES_OPTION;
+
const BYTE_PERMIT_UNIT: u64 = 1024 * 1024;
+const DEFAULT_BYTE_OPTION: &str = "the row-group read budget";
const DEFAULT_PARALLELISM: usize = 8;
const DEFAULT_MAX_INFLIGHT_BYTES: u64 = 256 * 1024 * 1024;
-/// Shared resource budget for concurrent Parquet row-group reads.
+/// Resource budget for concurrent row-group reads, bounding both the number of
+/// row groups in flight and their estimated bytes. Parquet holds one per scan;
+/// Mosaic builds one per open file from its own prefetch options.
#[derive(Debug)]
-pub struct ParquetReadBudget {
+pub struct ReadBudget {
parallelism: usize,
row_groups: Arc<Semaphore>,
bytes: Arc<Semaphore>,
byte_permits: u32,
+ byte_permit_unit: u64,
max_inflight_bytes: u64,
+ byte_option: &'static str,
oversized_warning_logged: AtomicBool,
- diagnostics: Arc<ParquetReadDiagnostics>,
+ diagnostics: Arc<ReadBudgetDiagnostics>,
}
#[derive(Debug)]
-struct ParquetReadDiagnostics {
+struct ReadBudgetDiagnostics {
enabled: AtomicBool,
row_group_count: AtomicU64,
projected_bytes_min: AtomicU64,
@@ -47,7 +54,7 @@ struct ParquetReadDiagnostics {
peak_inflight: AtomicUsize,
}
-impl Default for ParquetReadDiagnostics {
+impl Default for ReadBudgetDiagnostics {
fn default() -> Self {
Self {
enabled: AtomicBool::new(false),
@@ -62,7 +69,7 @@ impl Default for ParquetReadDiagnostics {
}
#[derive(Debug, Default, PartialEq, Eq)]
-pub(crate) struct ParquetReadDiagnosticsSnapshot {
+pub(crate) struct ReadBudgetDiagnosticsSnapshot {
pub(crate) row_group_count: u64,
pub(crate) projected_bytes_min: u64,
pub(crate) projected_bytes_max: u64,
@@ -71,12 +78,28 @@ pub(crate) struct ParquetReadDiagnosticsSnapshot {
pub(crate) peak_inflight: usize,
}
-impl ParquetReadBudget {
+impl ReadBudget {
+ /// Scan-wide budget with MiB-granular byte permits, as the Parquet reader
+ /// takes it; its oversized-row-group warning names the Parquet option.
pub fn new(parallelism: usize, max_inflight_bytes: u64) ->
crate::Result<Self> {
+ Ok(
+ Self::with_byte_granularity(parallelism, max_inflight_bytes,
BYTE_PERMIT_UNIT)?
+ .with_byte_option(PARQUET_ROW_GROUP_MAX_INFLIGHT_BYTES_OPTION),
+ )
+ }
+
+ /// Budget whose byte permits count `byte_permit_unit` bytes each. Mosaic
+ /// charges the exact estimated bytes of a row group, so it passes `1`; at
+ /// most `u32::MAX` permits are tracked, which caps that unit at 4 GiB.
+ pub(crate) fn with_byte_granularity(
+ parallelism: usize,
+ max_inflight_bytes: u64,
+ byte_permit_unit: u64,
+ ) -> crate::Result<Self> {
if parallelism == 0 || parallelism > Semaphore::MAX_PERMITS {
return Err(crate::Error::DataInvalid {
message: format!(
- "Parquet row-group parallelism must be between 1 and {},
got {parallelism}",
+ "Row-group read parallelism must be between 1 and {}, got
{parallelism}",
Semaphore::MAX_PERMITS
),
source: None,
@@ -84,13 +107,14 @@ impl ParquetReadBudget {
}
if max_inflight_bytes == 0 {
return Err(crate::Error::DataInvalid {
- message: "Parquet row-group max in-flight bytes must be
greater than 0".to_string(),
+ message: "Row-group read max in-flight bytes must be greater
than 0".to_string(),
source: None,
});
}
+ let byte_permit_unit = byte_permit_unit.max(1);
let max_byte_permits = Semaphore::MAX_PERMITS.min(u32::MAX as usize)
as u32;
let byte_permits = max_inflight_bytes
- .div_ceil(BYTE_PERMIT_UNIT)
+ .div_ceil(byte_permit_unit)
.min(u64::from(max_byte_permits)) as u32;
Ok(Self {
@@ -98,12 +122,20 @@ impl ParquetReadBudget {
row_groups: Arc::new(Semaphore::new(parallelism)),
bytes: Arc::new(Semaphore::new(byte_permits as usize)),
byte_permits,
+ byte_permit_unit,
max_inflight_bytes,
+ byte_option: DEFAULT_BYTE_OPTION,
oversized_warning_logged: AtomicBool::new(false),
- diagnostics: Arc::new(ParquetReadDiagnostics::default()),
+ diagnostics: Arc::new(ReadBudgetDiagnostics::default()),
})
}
+ /// Name the table option that set the byte budget, for the oversized
warning.
+ pub(crate) fn with_byte_option(mut self, byte_option: &'static str) ->
Self {
+ self.byte_option = byte_option;
+ self
+ }
+
pub fn parallelism(&self) -> usize {
self.parallelism
}
@@ -140,9 +172,9 @@ impl ParquetReadBudget {
);
}
- pub(crate) fn diagnostics(&self) -> ParquetReadDiagnosticsSnapshot {
+ pub(crate) fn diagnostics(&self) -> ReadBudgetDiagnosticsSnapshot {
let row_group_count =
self.diagnostics.row_group_count.load(Ordering::Relaxed);
- ParquetReadDiagnosticsSnapshot {
+ ReadBudgetDiagnosticsSnapshot {
row_group_count,
projected_bytes_min: if row_group_count == 0 {
0
@@ -159,39 +191,62 @@ impl ParquetReadBudget {
}
}
- pub(crate) async fn acquire(
- &self,
- projected_uncompressed_bytes: u64,
- ) -> crate::Result<ParquetReadPermit> {
+ /// Wait for a row-group slot and the estimated bytes. The Parquet reader
+ /// acquires from its async tasks.
+ pub(crate) async fn acquire(&self, estimated_bytes: u64) ->
crate::Result<ReadPermit> {
let row_group = Arc::clone(&self.row_groups)
.acquire_owned()
.await
- .map_err(|error| crate::Error::UnexpectedError {
- message: "Parquet row-group read budget was
closed".to_string(),
- source: Some(Box::new(error)),
- })?;
- let requested = projected_uncompressed_bytes
- .max(1)
- .div_ceil(BYTE_PERMIT_UNIT)
- .min(u64::from(self.byte_permits)) as u32;
- if projected_uncompressed_bytes > self.max_inflight_bytes
+ .map_err(|_| Self::closed("row-group"))?;
+ let bytes = Arc::clone(&self.bytes)
+ .acquire_many_owned(self.byte_permits_for(estimated_bytes))
+ .await
+ .map_err(|_| Self::closed("byte"))?;
+ Ok(self.permit(row_group, bytes))
+ }
+
+ /// Take a row-group slot and the estimated bytes if both are free now. The
+ /// Mosaic reader refills its look-ahead without waiting.
+ pub(crate) fn try_acquire(&self, estimated_bytes: u64) ->
Option<ReadPermit> {
+ let row_group = Arc::clone(&self.row_groups).try_acquire_owned().ok()?;
+ let bytes = Arc::clone(&self.bytes)
+ .try_acquire_many_owned(self.byte_permits_for(estimated_bytes))
+ .ok()?;
+ Some(self.permit(row_group, bytes))
+ }
+
+ /// Block until a row-group slot and the estimated bytes are free. The
+ /// Mosaic reader decodes on its own threads, so it waits here.
+ pub(crate) fn acquire_blocking(&self, estimated_bytes: u64) ->
crate::Result<ReadPermit> {
+ futures::executor::block_on(self.acquire(estimated_bytes))
+ }
+
+ fn closed(resource: &str) -> crate::Error {
+ crate::Error::UnexpectedError {
+ message: format!("The {resource} read budget was closed"),
+ source: None,
+ }
+ }
+
+ fn byte_permits_for(&self, estimated_bytes: u64) -> u32 {
+ if estimated_bytes > self.max_inflight_bytes
&& !self.oversized_warning_logged.swap(true, Ordering::Relaxed)
{
log::warn!(
- "Parquet row group projected size
({projected_uncompressed_bytes} bytes) exceeds \
- read.parquet.row-group.max-inflight-bytes ({} bytes); it will
consume the entire \
- byte budget and may reduce row-group read parallelism;
increase the option if \
- memory allows",
+ "A row group's estimated size ({estimated_bytes} bytes)
exceeds {} ({} bytes); it \
+ will consume the entire byte budget and may reduce row-group
read parallelism; \
+ increase the option if memory allows",
+ self.byte_option,
self.max_inflight_bytes
);
}
- let bytes = Arc::clone(&self.bytes)
- .acquire_many_owned(requested)
- .await
- .map_err(|error| crate::Error::UnexpectedError {
- message: "Parquet byte read budget was closed".to_string(),
- source: Some(Box::new(error)),
- })?;
+ estimated_bytes
+ .max(1)
+ .div_ceil(self.byte_permit_unit)
+ .min(u64::from(self.byte_permits)) as u32
+ }
+
+ fn permit(&self, row_group: OwnedSemaphorePermit, bytes:
OwnedSemaphorePermit) -> ReadPermit {
let diagnostics = self.diagnostics_enabled().then(|| {
let current = self
.diagnostics
@@ -203,15 +258,15 @@ impl ParquetReadBudget {
.fetch_max(current, Ordering::Relaxed);
Arc::clone(&self.diagnostics)
});
- Ok(ParquetReadPermit {
+ ReadPermit {
_row_group: row_group,
_bytes: bytes,
diagnostics,
- })
+ }
}
}
-impl Default for ParquetReadBudget {
+impl Default for ReadBudget {
fn default() -> Self {
Self::new(DEFAULT_PARALLELISM, DEFAULT_MAX_INFLIGHT_BYTES)
.expect("default Parquet read budget is valid")
@@ -219,13 +274,13 @@ impl Default for ParquetReadBudget {
}
#[derive(Debug)]
-pub(crate) struct ParquetReadPermit {
+pub(crate) struct ReadPermit {
_row_group: OwnedSemaphorePermit,
_bytes: OwnedSemaphorePermit,
- diagnostics: Option<Arc<ParquetReadDiagnostics>>,
+ diagnostics: Option<Arc<ReadBudgetDiagnostics>>,
}
-impl Drop for ParquetReadPermit {
+impl Drop for ReadPermit {
fn drop(&mut self) {
if let Some(diagnostics) = &self.diagnostics {
diagnostics.current_inflight.fetch_sub(1, Ordering::Relaxed);
@@ -240,7 +295,7 @@ mod tests {
#[tokio::test]
async fn shared_budget_blocks_until_permits_are_released() {
- let budget = Arc::new(ParquetReadBudget::new(2,
BYTE_PERMIT_UNIT).unwrap());
+ let budget = Arc::new(ReadBudget::new(2, BYTE_PERMIT_UNIT).unwrap());
let first = budget.acquire(2 * BYTE_PERMIT_UNIT).await.unwrap();
assert!(
@@ -259,7 +314,7 @@ mod tests {
#[tokio::test]
async fn diagnostics_aggregate_shared_row_group_reads() {
- let budget = Arc::new(ParquetReadBudget::new(2, 2 *
BYTE_PERMIT_UNIT).unwrap());
+ let budget = Arc::new(ReadBudget::new(2, 2 *
BYTE_PERMIT_UNIT).unwrap());
budget.enable_diagnostics();
budget.record_projected_row_groups(&[300, 100, 200]);
@@ -267,7 +322,7 @@ mod tests {
let second = budget.acquire(1).await.unwrap();
assert_eq!(
budget.diagnostics(),
- ParquetReadDiagnosticsSnapshot {
+ ReadBudgetDiagnosticsSnapshot {
row_group_count: 3,
projected_bytes_min: 100,
projected_bytes_max: 300,
@@ -285,18 +340,17 @@ mod tests {
#[test]
fn rejects_invalid_limits() {
- assert!(ParquetReadBudget::new(0, BYTE_PERMIT_UNIT).is_err());
- assert!(ParquetReadBudget::new(1, 0).is_err());
+ assert!(ReadBudget::new(0, BYTE_PERMIT_UNIT).is_err());
+ assert!(ReadBudget::new(1, 0).is_err());
assert!(
- ParquetReadBudget::new(Semaphore::MAX_PERMITS.saturating_add(1),
BYTE_PERMIT_UNIT)
- .is_err()
+ ReadBudget::new(Semaphore::MAX_PERMITS.saturating_add(1),
BYTE_PERMIT_UNIT).is_err()
);
}
#[tokio::test]
async fn oversized_row_group_consumes_the_budget() {
let max_inflight_bytes = 8 * BYTE_PERMIT_UNIT + 1;
- let budget = Arc::new(ParquetReadBudget::new(8,
max_inflight_bytes).unwrap());
+ let budget = Arc::new(ReadBudget::new(8, max_inflight_bytes).unwrap());
let first = budget.acquire(max_inflight_bytes).await.unwrap();
assert!(!budget.oversized_warning_logged.load(Ordering::Relaxed));
assert!(
@@ -314,7 +368,7 @@ mod tests {
#[tokio::test]
async fn small_row_groups_keep_exact_accounting() {
- let budget = Arc::new(ParquetReadBudget::new(4, 4 *
BYTE_PERMIT_UNIT).unwrap());
+ let budget = Arc::new(ReadBudget::new(4, 4 *
BYTE_PERMIT_UNIT).unwrap());
let mut permits = Vec::new();
for _ in 0..4 {
permits.push(budget.acquire(BYTE_PERMIT_UNIT).await.unwrap());
@@ -328,7 +382,7 @@ mod tests {
#[tokio::test]
async fn tiny_budget_still_admits_one_at_a_time() {
- let budget = Arc::new(ParquetReadBudget::new(8,
BYTE_PERMIT_UNIT).unwrap());
+ let budget = Arc::new(ReadBudget::new(8, BYTE_PERMIT_UNIT).unwrap());
let first = budget.acquire(100 * BYTE_PERMIT_UNIT).await.unwrap();
assert!(
tokio::time::timeout(Duration::from_millis(20), budget.acquire(1))
@@ -339,4 +393,49 @@ mod tests {
drop(first);
budget.acquire(1).await.unwrap();
}
+
+ #[test]
+ fn
try_acquire_is_cumulative_and_an_oversized_head_takes_the_whole_budget() {
+ // Byte-exact granularity, the way the Mosaic reader charges row
groups.
+ let budget = ReadBudget::with_byte_granularity(8, 20, 1).unwrap();
+ let first = budget.try_acquire(10).unwrap();
+ let second = budget.try_acquire(10).unwrap();
+ assert!(budget.try_acquire(10).is_none());
+ drop(first);
+ let third = budget.try_acquire(10).unwrap();
+ drop((second, third));
+
+ let oversized = budget.try_acquire(21).unwrap();
+ assert!(budget.try_acquire(1).is_none());
+ drop(oversized);
+ assert!(budget.try_acquire(1).is_some());
+
+ let slot_limited = ReadBudget::with_byte_granularity(2, u64::MAX,
1).unwrap();
+ let first = slot_limited.try_acquire(1).unwrap();
+ let second = slot_limited.try_acquire(1).unwrap();
+ assert!(slot_limited.try_acquire(1).is_none());
+ drop((first, second));
+ }
+
+ #[test]
+ fn acquire_blocking_waits_off_the_runtime_until_a_permit_is_released() {
+ let budget = Arc::new(ReadBudget::with_byte_granularity(1, 10,
1).unwrap());
+ let held = budget.try_acquire(10).unwrap();
+ let (tx, rx) = std::sync::mpsc::channel();
+ let waiter = std::thread::spawn({
+ let budget = Arc::clone(&budget);
+ move || {
+ let permit = budget.acquire_blocking(10);
+ tx.send(()).ok();
+ permit
+ }
+ });
+
+ assert!(
+ rx.recv_timeout(Duration::from_millis(20)).is_err(),
+ "the budget must hold the waiter until a permit is released"
+ );
+ drop(held);
+ waiter.join().unwrap().unwrap();
+ }
}
diff --git a/crates/paimon/src/spec/core_options.rs
b/crates/paimon/src/spec/core_options.rs
index 51b51e55..a741a087 100644
--- a/crates/paimon/src/spec/core_options.rs
+++ b/crates/paimon/src/spec/core_options.rs
@@ -93,8 +93,10 @@ 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_ROW_GROUP_PARALLELISM_OPTION: &str =
"read.parquet.row-group.parallelism";
-const PARQUET_ROW_GROUP_MAX_INFLIGHT_BYTES_OPTION: &str =
+pub(crate) const PARQUET_ROW_GROUP_MAX_INFLIGHT_BYTES_OPTION: &str =
"read.parquet.row-group.max-inflight-bytes";
+const MOSAIC_READ_PREFETCH_ROW_GROUPS_OPTION: &str =
"mosaic.read.prefetch-row-groups";
+pub(crate) const MOSAIC_READ_PREFETCH_MAX_BYTES_OPTION: &str =
"mosaic.read.prefetch-max-bytes";
pub(crate) const TABLE_READ_SEQUENCE_NUMBER_ENABLED_OPTION: &str =
"table-read.sequence-number.enabled";
pub(crate) const SEQUENCE_FIELD_OPTION: &str = "sequence.field";
@@ -142,6 +144,8 @@ const DEFAULT_WRITE_PARQUET_BUFFER_SIZE: i64 = 256 * 1024 *
1024;
const DEFAULT_READ_BATCH_SIZE: usize = 1024;
const DEFAULT_PARQUET_ROW_GROUP_PARALLELISM: usize = 8;
const DEFAULT_PARQUET_ROW_GROUP_MAX_INFLIGHT_BYTES: i64 = 256 * 1024 * 1024;
+const DEFAULT_MOSAIC_READ_PREFETCH_ROW_GROUPS: usize = 8;
+const DEFAULT_MOSAIC_READ_PREFETCH_MAX_BYTES: i64 = 64 * 1024 * 1024;
const DYNAMIC_BUCKET_TARGET_ROW_NUM_OPTION: &str =
"dynamic-bucket.target-row-num";
const DEFAULT_DYNAMIC_BUCKET_TARGET_ROW_NUM: i64 = 200_000;
const DEFAULT_GLOBAL_INDEX_ROW_COUNT_PER_SHARD: i64 = 100_000;
@@ -404,6 +408,41 @@ impl<'a> CoreOptions<'a> {
Ok(value)
}
+ /// Mosaic row groups kept opening ahead of the consumer per file; `0`
disables prefetch.
+ pub fn mosaic_read_prefetch_row_groups(&self) -> crate::Result<usize> {
+ let Some(raw) =
self.options.get(MOSAIC_READ_PREFETCH_ROW_GROUPS_OPTION) else {
+ return Ok(DEFAULT_MOSAIC_READ_PREFETCH_ROW_GROUPS);
+ };
+ raw.parse::<usize>().map_err(|error| crate::Error::DataInvalid {
+ message: format!(
+ "Option '{MOSAIC_READ_PREFETCH_ROW_GROUPS_OPTION}' must be a
non-negative integer, got: {raw}"
+ ),
+ source: Some(Box::new(error)),
+ })
+ }
+
+ /// Estimated decoded bytes a Mosaic file may hold in flight beyond the
head row group.
+ pub fn mosaic_read_prefetch_max_bytes(&self) -> crate::Result<u64> {
+ let value = match
self.options.get(MOSAIC_READ_PREFETCH_MAX_BYTES_OPTION) {
+ Some(raw) => parse_memory_size(raw).ok_or_else(||
crate::Error::DataInvalid {
+ message: format!(
+ "Option '{MOSAIC_READ_PREFETCH_MAX_BYTES_OPTION}' must be
a valid memory size, got: {raw}"
+ ),
+ source: None,
+ })?,
+ None => DEFAULT_MOSAIC_READ_PREFETCH_MAX_BYTES,
+ };
+ u64::try_from(value)
+ .ok()
+ .filter(|value| *value > 0)
+ .ok_or_else(|| crate::Error::DataInvalid {
+ message: format!(
+ "Option '{MOSAIC_READ_PREFETCH_MAX_BYTES_OPTION}' must be
greater than 0, got: {value}"
+ ),
+ source: None,
+ })
+ }
+
/// Scan-wide projected uncompressed bytes for concurrent Parquet row
groups.
pub fn parquet_row_group_max_inflight_bytes(&self) -> crate::Result<u64> {
let value = match self
@@ -1655,6 +1694,53 @@ mod tests {
assert!(!CoreOptions::new(&options).file_index_read_enabled());
}
+ #[test]
+ fn test_mosaic_read_prefetch_options() {
+ let options = HashMap::new();
+ let core = CoreOptions::new(&options);
+ assert_eq!(core.mosaic_read_prefetch_row_groups().unwrap(), 8);
+ assert_eq!(
+ core.mosaic_read_prefetch_max_bytes().unwrap(),
+ 64 * 1024 * 1024
+ );
+
+ let options = HashMap::from([
+ (
+ MOSAIC_READ_PREFETCH_ROW_GROUPS_OPTION.to_string(),
+ "0".to_string(),
+ ),
+ (
+ MOSAIC_READ_PREFETCH_MAX_BYTES_OPTION.to_string(),
+ "16 mb".to_string(),
+ ),
+ ]);
+ let core = CoreOptions::new(&options);
+ assert_eq!(core.mosaic_read_prefetch_row_groups().unwrap(), 0);
+ assert_eq!(
+ core.mosaic_read_prefetch_max_bytes().unwrap(),
+ 16 * 1024 * 1024
+ );
+
+ for value in ["-1", "invalid", "1.5"] {
+ let options = HashMap::from([(
+ MOSAIC_READ_PREFETCH_ROW_GROUPS_OPTION.to_string(),
+ value.to_string(),
+ )]);
+ assert!(CoreOptions::new(&options)
+ .mosaic_read_prefetch_row_groups()
+ .is_err());
+ }
+ for value in ["0", "-1", "invalid", "9223372036854775807 tb"] {
+ let options = HashMap::from([(
+ MOSAIC_READ_PREFETCH_MAX_BYTES_OPTION.to_string(),
+ value.to_string(),
+ )]);
+ assert!(CoreOptions::new(&options)
+ .mosaic_read_prefetch_max_bytes()
+ .is_err());
+ }
+ }
+
#[test]
fn test_parquet_row_group_read_budget_options() {
let options = HashMap::new();
diff --git a/crates/paimon/src/table/audit_log_table/read.rs
b/crates/paimon/src/table/audit_log_table/read.rs
index 625d3170..5b4aac43 100644
--- a/crates/paimon/src/table/audit_log_table/read.rs
+++ b/crates/paimon/src/table/audit_log_table/read.rs
@@ -28,6 +28,7 @@ use crate::spec::{
};
use crate::table::data_file_reader::DataFileReader;
use crate::table::kv_file_reader::{KeyValueFileReader, KeyValueReadConfig};
+use crate::table::table_read::configured_mosaic_prefetch;
use crate::table::ArrowRecordBatchStream;
use crate::DataSplit;
use arrow_array::{ArrayRef, RecordBatch, RecordBatchOptions, StringArray};
@@ -125,6 +126,7 @@ impl<'a> AuditLogRead<'a> {
merge_splits: false,
max_merge_input_streams: Some(MAX_MERGE_INPUT_STREAMS),
parquet_read_budget: Some(parquet_read_budget),
+ mosaic_prefetch:
configured_mosaic_prefetch(self.read.table)?,
},
)
.read_with_merge_function(
diff --git a/crates/paimon/src/table/data_evolution_reader.rs
b/crates/paimon/src/table/data_evolution_reader.rs
index c162c7e0..ce0ece4d 100644
--- a/crates/paimon/src/table/data_evolution_reader.rs
+++ b/crates/paimon/src/table/data_evolution_reader.rs
@@ -24,7 +24,8 @@ use super::data_file_reader::{
};
use crate::arrow::format::blob::DEFAULT_BLOB_READ_PARALLELISM;
use crate::arrow::format::FilePredicates;
-use crate::arrow::{build_target_arrow_schema, ParquetReadBudget};
+use crate::arrow::format::MosaicPrefetchOptions;
+use crate::arrow::{build_target_arrow_schema, ReadBudget};
use crate::deletion_vector::{DeletionVector, DeletionVectorFactory};
use crate::io::FileIO;
use crate::spec::{
@@ -115,7 +116,8 @@ pub(crate) struct DataEvolutionReader {
blob_read_limiter: BlobReadLimiter,
blob_parallelism: usize,
batch_size: Option<usize>,
- parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ parquet_read_budget: Option<Arc<ReadBudget>>,
+ mosaic_prefetch: MosaicPrefetchOptions,
read_timing: Option<Arc<DataFileReadTiming>>,
}
@@ -195,6 +197,7 @@ impl DataEvolutionReader {
blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
batch_size: None,
parquet_read_budget: None,
+ mosaic_prefetch: MosaicPrefetchOptions::default(),
read_timing: None,
})
}
@@ -213,12 +216,17 @@ impl DataEvolutionReader {
pub(crate) fn with_parquet_read_budget(
mut self,
- parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ parquet_read_budget: Option<Arc<ReadBudget>>,
) -> Self {
self.parquet_read_budget = parquet_read_budget;
self
}
+ pub(crate) fn with_mosaic_prefetch(mut self, mosaic_prefetch:
MosaicPrefetchOptions) -> Self {
+ self.mosaic_prefetch = mosaic_prefetch;
+ self
+ }
+
pub(crate) fn with_read_timing(mut self, read_timing:
Option<Arc<DataFileReadTiming>>) -> Self {
self.read_timing = read_timing;
self
@@ -267,6 +275,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_mosaic_prefetch(self.mosaic_prefetch)
.with_read_timing(self.read_timing.clone());
for split in splits {
@@ -566,7 +575,8 @@ impl DataEvolutionReader {
None,
)?
.with_batch_size(self.batch_size)
- .with_parquet_read_budget(self.parquet_read_budget.clone());
+ .with_parquet_read_budget(self.parquet_read_budget.clone())
+ .with_mosaic_prefetch(self.mosaic_prefetch);
let mut stream = prescan.read(splits)?;
let mut view_structs = HashSet::new();
while let Some(batch) = stream.next().await {
@@ -631,6 +641,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 mosaic_prefetch = self.mosaic_prefetch;
let read_timing = self.read_timing.clone();
let anchor_deletion_vector = anchor_deletion_vector.clone();
// Batch size for column-merge output. Matches the default Parquet
reader batch size.
@@ -719,6 +730,7 @@ impl DataEvolutionReader {
blob_as_descriptor,
blob_parallelism,
source_parquet_read_budget.clone(),
+ mosaic_prefetch,
read_timing.clone(),
anchor_deletion_vector.as_ref(),
)
@@ -1258,7 +1270,8 @@ fn open_source_stream(
batch_size: Option<usize>,
blob_as_descriptor: bool,
blob_parallelism: usize,
- parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ parquet_read_budget: Option<Arc<ReadBudget>>,
+ mosaic_prefetch: MosaicPrefetchOptions,
read_timing: Option<Arc<DataFileReadTiming>>,
anchor_deletion_vector: Option<&DeletionVectorContext>,
) -> crate::Result<ArrowRecordBatchStream> {
@@ -1328,6 +1341,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_mosaic_prefetch(mosaic_prefetch)
.with_read_timing(read_timing);
match source {
diff --git a/crates/paimon/src/table/data_file_reader.rs
b/crates/paimon/src/table/data_file_reader.rs
index b24f03ad..7019e9bc 100644
--- a/crates/paimon/src/table/data_file_reader.rs
+++ b/crates/paimon/src/table/data_file_reader.rs
@@ -17,9 +17,9 @@
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::format::{create_format_reader_with_budget,
MosaicPrefetchOptions};
use crate::arrow::schema_evolution::{create_index_mapping, NULL_FIELD_INDEX};
-use crate::arrow::ParquetReadBudget;
+use crate::arrow::ReadBudget;
use crate::deletion_vector::{DeletionVector, DeletionVectorFactory};
use crate::file_index::evaluator::evaluate_file_index;
use crate::file_index::file_index_result::FileIndexResult;
@@ -120,7 +120,8 @@ pub(crate) struct DataFileReader {
blob_as_descriptor: bool,
blob_parallelism: usize,
batch_size: Option<usize>,
- parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ parquet_read_budget: Option<Arc<ReadBudget>>,
+ mosaic_prefetch: MosaicPrefetchOptions,
read_timing: Option<Arc<DataFileReadTiming>>,
}
@@ -146,6 +147,7 @@ impl DataFileReader {
blob_parallelism: DEFAULT_BLOB_READ_PARALLELISM,
batch_size: None,
parquet_read_budget: None,
+ mosaic_prefetch: MosaicPrefetchOptions::default(),
read_timing: None,
}
}
@@ -173,12 +175,17 @@ impl DataFileReader {
pub(crate) fn with_parquet_read_budget(
mut self,
- parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ parquet_read_budget: Option<Arc<ReadBudget>>,
) -> Self {
self.parquet_read_budget = parquet_read_budget;
self
}
+ pub(crate) fn with_mosaic_prefetch(mut self, mosaic_prefetch:
MosaicPrefetchOptions) -> Self {
+ self.mosaic_prefetch = mosaic_prefetch;
+ self
+ }
+
pub(crate) fn with_read_timing(mut self, read_timing:
Option<Arc<DataFileReadTiming>>) -> Self {
self.read_timing = read_timing;
self
@@ -460,6 +467,7 @@ impl DataFileReader {
let blob_parallelism = self.blob_parallelism;
let batch_size = self.batch_size;
let parquet_read_budget = self.parquet_read_budget.clone();
+ let mosaic_prefetch = self.mosaic_prefetch;
let read_timing = self.read_timing.clone();
let target_schema = build_target_arrow_schema(&read_type)?;
@@ -520,6 +528,7 @@ impl DataFileReader {
&format_read_fields,
parquet_read_budget,
blob_parallelism,
+ mosaic_prefetch,
)?;
let input_file = file_io.new_input(&path_to_read)?;
let open_start = read_timing.as_ref().map(|_| Instant::now());
@@ -729,6 +738,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 mosaic_prefetch = self.mosaic_prefetch;
let target_schema = build_target_arrow_schema(&read_type)?;
let file_fields = data_fields.clone().unwrap_or_else(||
table_fields.clone());
@@ -794,6 +804,7 @@ impl DataFileReader {
&format_read_fields,
parquet_read_budget,
blob_parallelism,
+ mosaic_prefetch,
)?;
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 f426e096..2e90822d 100644
--- a/crates/paimon/src/table/format_read_builder.rs
+++ b/crates/paimon/src/table/format_read_builder.rs
@@ -23,7 +23,7 @@ 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::arrow::ReadBudget;
use crate::spec::{DataField, Predicate};
use crate::table::source::RowRange;
use crate::Result;
@@ -42,7 +42,7 @@ pub(crate) struct FormatReadBuilder<'a> {
limit: Option<usize>,
row_ranges: Option<Vec<RowRange>>,
case_sensitive: bool,
- parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ parquet_read_budget: Option<Arc<ReadBudget>>,
blob_parallelism: usize,
}
@@ -113,7 +113,7 @@ impl<'a> FormatReadBuilder<'a> {
self
}
- pub(crate) fn with_parquet_read_budget(&mut self, budget:
Arc<ParquetReadBudget>) -> &mut Self {
+ pub(crate) fn with_parquet_read_budget(&mut self, budget: Arc<ReadBudget>)
-> &mut Self {
self.parquet_read_budget = Some(budget);
self
}
diff --git a/crates/paimon/src/table/format_table_read.rs
b/crates/paimon/src/table/format_table_read.rs
index c20c3839..d2281405 100644
--- a/crates/paimon/src/table/format_table_read.rs
+++ b/crates/paimon/src/table/format_table_read.rs
@@ -19,11 +19,11 @@
use super::data_file_reader::DataFileReader;
use super::read_builder::split_scan_predicates;
-use super::table_read::configured_parquet_read_budget;
+use super::table_read::{configured_mosaic_prefetch,
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::arrow::{build_target_arrow_schema, ReadBudget};
use crate::spec::{DataField, Predicate};
use crate::{DataSplit, Error};
use arrow_array::{RecordBatch, RecordBatchOptions};
@@ -37,7 +37,7 @@ pub(crate) struct FormatTableRead<'a> {
read_type: Vec<DataField>,
data_predicates: Vec<Predicate>,
row_filter_factory: Option<Arc<dyn crate::arrow::RowFilterFactory>>,
- parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ parquet_read_budget: Option<Arc<ReadBudget>>,
limit: Option<usize>,
blob_parallelism: usize,
}
@@ -85,7 +85,7 @@ impl<'a> FormatTableRead<'a> {
self
}
- pub(crate) fn with_parquet_read_budget(mut self, budget:
Arc<ParquetReadBudget>) -> Self {
+ pub(crate) fn with_parquet_read_budget(mut self, budget: Arc<ReadBudget>)
-> Self {
self.parquet_read_budget = Some(budget);
self
}
@@ -95,7 +95,7 @@ impl<'a> FormatTableRead<'a> {
self
}
- fn parquet_read_budget(&self) -> crate::Result<Arc<ParquetReadBudget>> {
+ fn parquet_read_budget(&self) -> crate::Result<Arc<ReadBudget>> {
match &self.parquet_read_budget {
Some(budget) => Ok(Arc::clone(budget)),
None => configured_parquet_read_budget(self.table),
@@ -127,6 +127,7 @@ impl<'a> FormatTableRead<'a> {
let schema_id = self.table.schema().id();
let mut remaining = self.limit;
let batch_size = Some(core_options.read_batch_size()?);
+ 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 blob_parallelism = self.blob_parallelism;
@@ -147,7 +148,8 @@ impl<'a> FormatTableRead<'a> {
)
.with_batch_size(batch_size)
.with_blob_parallelism(blob_parallelism)
- .with_parquet_read_budget(parquet_read_budget.clone());
+ .with_parquet_read_budget(parquet_read_budget.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/kv_file_reader.rs
b/crates/paimon/src/table/kv_file_reader.rs
index 8bc665e1..0bcc97cd 100644
--- a/crates/paimon/src/table/kv_file_reader.rs
+++ b/crates/paimon/src/table/kv_file_reader.rs
@@ -30,7 +30,8 @@ use super::sort_merge::{
AggregateMergeFunction, DeduplicateMergeFunction, FirstRowMergeFunction,
MergeFunction,
PartialUpdateMergeFunction, SortMergeReaderBuilder,
};
-use crate::arrow::{build_target_arrow_schema, ParquetReadBudget};
+use crate::arrow::format::MosaicPrefetchOptions;
+use crate::arrow::{build_target_arrow_schema, ReadBudget};
use crate::deletion_vector::DeletionVectorFactory;
use crate::io::FileIO;
use crate::spec::{
@@ -81,7 +82,9 @@ pub(crate) struct KeyValueReadConfig {
/// This limits merge fan-in, not files: files within a run are opened
serially.
pub max_merge_input_streams: Option<usize>,
/// Scan-shared Parquet concurrency and projected-byte budget.
- pub parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ pub parquet_read_budget: Option<Arc<ReadBudget>>,
+ /// Per-file Mosaic row-group prefetch settings.
+ pub mosaic_prefetch: MosaicPrefetchOptions,
}
/// Keep only the conjuncts of `predicates` that reference primary-key columns,
@@ -588,7 +591,8 @@ impl KeyValueFileReader {
pushdown_predicates.clone(),
)
.with_batch_size(Some(config.read_batch_size))
-
.with_parquet_read_budget(group_parquet_read_budget.clone());
+
.with_parquet_read_budget(group_parquet_read_budget.clone())
+ .with_mosaic_prefetch(config.mosaic_prefetch);
let run_schema_manager = config.schema_manager.clone();
let run_file_io = file_io.clone();
let deletion_files_by_split =
deletion_files_by_split.clone();
@@ -1295,7 +1299,7 @@ mod tests {
file
}
- fn kv_reader_with_budget(table: &Table, budget: Arc<ParquetReadBudget>) ->
KeyValueFileReader {
+ fn kv_reader_with_budget(table: &Table, budget: Arc<ReadBudget>) ->
KeyValueFileReader {
let core_options = table.schema().core_options();
KeyValueFileReader::new(
table.file_io().clone(),
@@ -1314,6 +1318,7 @@ mod tests {
merge_splits: true,
max_merge_input_streams: None,
parquet_read_budget: Some(budget),
+ mosaic_prefetch: MosaicPrefetchOptions::default(),
},
)
}
@@ -1433,6 +1438,7 @@ mod tests {
merge_splits: true,
max_merge_input_streams: Some(256),
parquet_read_budget: None,
+ mosaic_prefetch: MosaicPrefetchOptions::default(),
},
);
@@ -1640,6 +1646,7 @@ mod tests {
merge_splits: false,
max_merge_input_streams: None,
parquet_read_budget: None,
+ mosaic_prefetch: MosaicPrefetchOptions::default(),
},
)
.with_input_batch_sizes(input_batch_sizes.clone());
@@ -1710,7 +1717,8 @@ mod tests {
read_batch_size: core_options.read_batch_size().unwrap(),
merge_splits: false,
max_merge_input_streams: None,
- parquet_read_budget: Some(Arc::new(ParquetReadBudget::new(2,
256 << 20).unwrap())),
+ parquet_read_budget: Some(Arc::new(ReadBudget::new(2, 256 <<
20).unwrap())),
+ mosaic_prefetch: MosaicPrefetchOptions::default(),
},
);
let batches = tokio::time::timeout(
@@ -1767,10 +1775,8 @@ mod tests {
assert_eq!(planned[0].len(), 1);
assert_eq!(planned[0][0].files.len(), 2);
- let reader = kv_reader_with_budget(
- &table,
- Arc::new(ParquetReadBudget::new(1, 256 << 20).unwrap()),
- );
+ let reader =
+ kv_reader_with_budget(&table, Arc::new(ReadBudget::new(1, 256 <<
20).unwrap()));
let batches = tokio::time::timeout(
std::time::Duration::from_secs(5),
reader
@@ -1826,7 +1832,7 @@ mod tests {
};
let first_split = split(vec![first_high, first_low]);
let second_split = split(vec![second_high, second_low]);
- let budget = Arc::new(ParquetReadBudget::new(1, 256 << 20).unwrap());
+ let budget = Arc::new(ReadBudget::new(1, 256 << 20).unwrap());
let first_reader = kv_reader_with_budget(&table, budget.clone());
let second_reader = kv_reader_with_budget(&table, budget);
@@ -1905,6 +1911,7 @@ mod tests {
merge_splits,
max_merge_input_streams: None,
parquet_read_budget: None,
+ mosaic_prefetch: MosaicPrefetchOptions::default(),
},
)
.read(splits)
@@ -1972,6 +1979,7 @@ mod tests {
merge_splits: true,
max_merge_input_streams: Some(256),
parquet_read_budget: None,
+ mosaic_prefetch: MosaicPrefetchOptions::default(),
},
);
let batches = reader
diff --git a/crates/paimon/src/table/read_builder.rs
b/crates/paimon/src/table/read_builder.rs
index f5087883..89883efa 100644
--- a/crates/paimon/src/table/read_builder.rs
+++ b/crates/paimon/src/table/read_builder.rs
@@ -259,10 +259,7 @@ impl<'a> ReadBuilder<'a> {
/// Inject a Parquet budget shared with sibling scan partitions.
#[doc(hidden)]
- pub fn with_parquet_read_budget(
- &mut self,
- budget: Arc<crate::arrow::ParquetReadBudget>,
- ) -> &mut Self {
+ pub fn with_parquet_read_budget(&mut self, budget:
Arc<crate::arrow::ReadBudget>) -> &mut Self {
match &mut self.0 {
ReadBuilderKind::Paimon(builder) => {
builder.with_parquet_read_budget(budget);
@@ -336,7 +333,7 @@ struct PaimonReadBuilder<'a> {
/// Kept apart so neither setter discards the other's constraint.
derived_row_ranges: Option<Vec<RowRange>>,
case_sensitive: bool,
- parquet_read_budget: Option<Arc<crate::arrow::ParquetReadBudget>>,
+ parquet_read_budget: Option<Arc<crate::arrow::ReadBudget>>,
blob_parallelism: usize,
}
@@ -485,10 +482,7 @@ impl<'a> PaimonReadBuilder<'a> {
self
}
- fn with_parquet_read_budget(
- &mut self,
- budget: Arc<crate::arrow::ParquetReadBudget>,
- ) -> &mut Self {
+ fn with_parquet_read_budget(&mut self, budget:
Arc<crate::arrow::ReadBudget>) -> &mut Self {
self.parquet_read_budget = Some(budget);
self
}
diff --git a/crates/paimon/src/table/table_read.rs
b/crates/paimon/src/table/table_read.rs
index 9d641841..47a0b0a7 100644
--- a/crates/paimon/src/table/table_read.rs
+++ b/crates/paimon/src/table/table_read.rs
@@ -24,7 +24,8 @@ 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::arrow::format::MosaicPrefetchOptions;
+use crate::arrow::ReadBudget;
use crate::spec::{
BigIntType, CoreOptions, DataField, DataType, MergeEngine, Predicate,
TinyIntType,
ROW_KIND_FIELD_ID, ROW_KIND_FIELD_NAME, SEQUENCE_NUMBER_FIELD_ID,
SEQUENCE_NUMBER_FIELD_NAME,
@@ -60,16 +61,22 @@ enum TableReadKind<'a> {
Format(FormatTableRead<'a>),
}
-pub(super) fn configured_parquet_read_budget(
- table: &Table,
-) -> crate::Result<Arc<ParquetReadBudget>> {
+pub(super) fn configured_parquet_read_budget(table: &Table) ->
crate::Result<Arc<ReadBudget>> {
let options = table.schema().core_options();
- Ok(Arc::new(ParquetReadBudget::new(
+ Ok(Arc::new(ReadBudget::new(
options.parquet_row_group_parallelism()?,
options.parquet_row_group_max_inflight_bytes()?,
)?))
}
+pub(crate) fn configured_mosaic_prefetch(table: &Table) ->
crate::Result<MosaicPrefetchOptions> {
+ let options = table.schema().core_options();
+ Ok(MosaicPrefetchOptions {
+ row_groups: options.mosaic_read_prefetch_row_groups()?,
+ max_bytes:
usize::try_from(options.mosaic_read_prefetch_max_bytes()?).unwrap_or(usize::MAX),
+ })
+}
+
impl<'a> TableRead<'a> {
/// Create a new TableRead with a specific read type (projected fields).
pub fn new(
@@ -170,7 +177,7 @@ impl<'a> TableRead<'a> {
/// Override the Parquet resource budget shared by this read.
#[doc(hidden)]
- pub fn with_parquet_read_budget(self, budget: Arc<ParquetReadBudget>) ->
Self {
+ pub fn with_parquet_read_budget(self, budget: Arc<ReadBudget>) -> Self {
match self.0 {
TableReadKind::Paimon(read) => {
Self(TableReadKind::Paimon(read.with_parquet_read_budget(budget)))
@@ -247,7 +254,7 @@ struct PaimonTableRead<'a> {
read_type: Vec<DataField>,
data_predicates: Vec<Predicate>,
row_filter_factory: Option<Arc<dyn crate::arrow::RowFilterFactory>>,
- parquet_read_budget: Option<Arc<ParquetReadBudget>>,
+ parquet_read_budget: Option<Arc<ReadBudget>>,
data_file_read_timing: Option<Arc<DataFileReadTiming>>,
blob_parallelism: usize,
}
@@ -305,7 +312,7 @@ impl<'a> PaimonTableRead<'a> {
self
}
- fn with_parquet_read_budget(mut self, budget: Arc<ParquetReadBudget>) ->
Self {
+ fn with_parquet_read_budget(mut self, budget: Arc<ReadBudget>) -> Self {
self.parquet_read_budget = Some(budget);
self
}
@@ -320,7 +327,7 @@ impl<'a> PaimonTableRead<'a> {
self
}
- fn parquet_read_budget(&self) -> crate::Result<Arc<ParquetReadBudget>> {
+ fn parquet_read_budget(&self) -> crate::Result<Arc<ReadBudget>> {
match &self.parquet_read_budget {
Some(budget) => Ok(Arc::clone(budget)),
None => configured_parquet_read_budget(self.table),
@@ -450,7 +457,8 @@ 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_parquet_read_budget(Some(self.parquet_read_budget()?))
+ .with_mosaic_prefetch(configured_mosaic_prefetch(self.table)?);
let raw_stream = reader.read(&data_splits)?;
Ok(Box::pin(async_stream::try_stream! {
@@ -748,6 +756,7 @@ impl<'a> PaimonTableRead<'a> {
// a row-group permit across yielded batches can otherwise let
// the first side block the second side indefinitely.
parquet_read_budget: None,
+ mosaic_prefetch: configured_mosaic_prefetch(self.table)?,
},
);
reader.read(splits)
@@ -904,6 +913,7 @@ impl<'a> PaimonTableRead<'a> {
&& core_options.deletion_vectors_merge_on_read())
.then_some(MAX_MERGE_INPUT_STREAMS),
parquet_read_budget: Some(self.parquet_read_budget()?),
+ mosaic_prefetch: configured_mosaic_prefetch(self.table)?,
},
);
reader.read(splits)
@@ -931,6 +941,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_mosaic_prefetch(configured_mosaic_prefetch(self.table)?)
.with_read_timing(self.data_file_read_timing.clone());
reader.read(data_splits)
}
@@ -954,6 +965,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_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.
// This constructor is also used by raw-convertible primary-key splits,
@@ -1791,11 +1803,49 @@ mod tests {
));
let read = TableRead::new(&table, table.schema.fields().to_vec(),
Vec::new())
-
.with_parquet_read_budget(Arc::new(ParquetReadBudget::default()));
+ .with_parquet_read_budget(Arc::new(ReadBudget::default()));
assert!(read.to_arrow(&[]).is_ok());
}
}
+ fn table_with_mosaic_prefetch_options(row_groups: &str, max_bytes: &str)
-> Table {
+ let schema = Schema::builder()
+ .column("id", DataType::Int(IntType::new()))
+ .option("mosaic.read.prefetch-row-groups", row_groups)
+ .option("mosaic.read.prefetch-max-bytes", max_bytes);
+ Table::new(
+ FileIOBuilder::new("memory").build().unwrap(),
+ Identifier::new("default", "mosaic_prefetch_t"),
+ "memory:/mosaic_prefetch_t".to_string(),
+ TableSchema::new(0, &schema.build().unwrap()),
+ None,
+ )
+ }
+
+ #[test]
+ fn test_mosaic_prefetch_options_come_from_table_options() {
+ let table = table_with_mosaic_prefetch_options("0", "16 mb");
+ assert_eq!(
+ configured_mosaic_prefetch(&table).unwrap(),
+ MosaicPrefetchOptions {
+ row_groups: 0,
+ max_bytes: 16 * 1024 * 1024,
+ }
+ );
+ let read = TableRead::new(&table, table.schema.fields().to_vec(),
Vec::new());
+ assert!(read.to_arrow(&[]).is_ok());
+
+ for (row_groups, max_bytes) in [("-1", "64 mb"), ("8", "0")] {
+ let table = table_with_mosaic_prefetch_options(row_groups,
max_bytes);
+ let read = TableRead::new(&table, table.schema.fields().to_vec(),
Vec::new());
+ assert!(matches!(
+ read.to_arrow(&[]),
+ Err(crate::Error::DataInvalid { ref message, .. })
+ if message.contains("mosaic.read.prefetch")
+ ));
+ }
+ }
+
#[test]
fn test_direct_incremental_read_fails_closed_when_query_auth_enabled() {
let table = query_auth_table();