delta_arrow_reader/reader/
options.rs1use std::collections::BTreeMap;
4
5use crate::{DeltaReaderError, error::InvalidConfigurationSnafu};
6
7const DEFAULT_MAX_CONCURRENT_FILE_READS_PER_PARTITION: usize = 3;
8const DEFAULT_OUTPUT_BUFFER_BATCHES_PER_PARTITION: usize = 1;
9const DEFAULT_PREFETCH_FILES_PER_PARTITION: usize = 2;
10const DEFAULT_PARQUET_METADATA_SIZE_HINT_BYTES: usize = 64 * 1024;
11pub(crate) const MAX_CONCURRENT_PARQUET_RANGE_READS: usize = 10;
12
13pub type DeltaStorageOptions = BTreeMap<String, String>;
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub enum DeltaSnapshotSelection {
19 #[default]
21 Latest,
22 Version(u64),
24}
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
28pub enum ParquetReaderBackend {
29 DeltaKernel,
31 #[default]
33 Direct,
34}
35
36#[doc(hidden)]
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
39pub enum ParquetRangeReadPolicy {
40 #[default]
42 Automatic,
43 ExactRanges,
45 MergeRangesWithinOneMegabyte,
47 StoreImplementation,
49}
50
51#[must_use = "execution options do nothing unless passed to a table or scan"]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct DeltaScanExecutionOptions {
55 parquet_backend: ParquetReaderBackend,
56 max_concurrent_file_reads_per_scan: Option<usize>,
57 max_concurrent_file_reads_per_partition: usize,
58 output_buffer_batches_per_partition: usize,
59 prefetch_files_per_partition: usize,
60 parquet_metadata_size_hint_bytes: Option<usize>,
61 parquet_full_file_read_threshold_bytes: Option<usize>,
62 parquet_range_read_policy: ParquetRangeReadPolicy,
63}
64
65impl DeltaScanExecutionOptions {
66 pub const fn new() -> Self {
68 Self {
69 parquet_backend: ParquetReaderBackend::Direct,
70 max_concurrent_file_reads_per_scan: None,
71 max_concurrent_file_reads_per_partition:
72 DEFAULT_MAX_CONCURRENT_FILE_READS_PER_PARTITION,
73 output_buffer_batches_per_partition: DEFAULT_OUTPUT_BUFFER_BATCHES_PER_PARTITION,
74 prefetch_files_per_partition: DEFAULT_PREFETCH_FILES_PER_PARTITION,
75 parquet_metadata_size_hint_bytes: Some(DEFAULT_PARQUET_METADATA_SIZE_HINT_BYTES),
76 parquet_full_file_read_threshold_bytes: None,
77 parquet_range_read_policy: ParquetRangeReadPolicy::Automatic,
78 }
79 }
80
81 pub const fn parquet_backend(&self) -> ParquetReaderBackend {
83 self.parquet_backend
84 }
85
86 pub const fn max_concurrent_file_reads_per_scan(&self) -> Option<usize> {
88 self.max_concurrent_file_reads_per_scan
89 }
90
91 pub const fn max_concurrent_file_reads_per_partition(&self) -> usize {
93 self.max_concurrent_file_reads_per_partition
94 }
95
96 pub const fn output_buffer_batches_per_partition(&self) -> usize {
98 self.output_buffer_batches_per_partition
99 }
100
101 pub const fn prefetch_files_per_partition(&self) -> usize {
103 self.prefetch_files_per_partition
104 }
105
106 pub const fn parquet_metadata_size_hint_bytes(&self) -> Option<usize> {
108 self.parquet_metadata_size_hint_bytes
109 }
110
111 pub const fn parquet_full_file_read_threshold_bytes(&self) -> Option<usize> {
113 self.parquet_full_file_read_threshold_bytes
114 }
115
116 pub(crate) const fn parquet_range_read_policy(&self) -> ParquetRangeReadPolicy {
117 self.parquet_range_read_policy
118 }
119
120 pub const fn with_parquet_backend(mut self, parquet_backend: ParquetReaderBackend) -> Self {
122 self.parquet_backend = parquet_backend;
123 self
124 }
125
126 #[doc(hidden)]
128 pub const fn with_parquet_range_read_policy(mut self, policy: ParquetRangeReadPolicy) -> Self {
129 self.parquet_range_read_policy = policy;
130 self
131 }
132
133 pub fn with_max_concurrent_file_reads_per_scan(
135 mut self,
136 max_concurrent_file_reads: Option<usize>,
137 ) -> Result<Self, DeltaReaderError> {
138 validate_optional_positive(
139 max_concurrent_file_reads,
140 "max_concurrent_file_reads_per_scan_must_be_positive",
141 )?;
142 self.max_concurrent_file_reads_per_scan = max_concurrent_file_reads;
143 Ok(self)
144 }
145
146 pub fn with_max_concurrent_file_reads_per_partition(
148 mut self,
149 max_concurrent_file_reads: usize,
150 ) -> Result<Self, DeltaReaderError> {
151 validate_positive(
152 max_concurrent_file_reads,
153 "max_concurrent_file_reads_per_partition_must_be_positive",
154 )?;
155 self.max_concurrent_file_reads_per_partition = max_concurrent_file_reads;
156 Ok(self)
157 }
158
159 pub fn with_output_buffer_batches_per_partition(
161 mut self,
162 output_buffer_batches: usize,
163 ) -> Result<Self, DeltaReaderError> {
164 validate_positive(
165 output_buffer_batches,
166 "output_buffer_batches_per_partition_must_be_positive",
167 )?;
168 self.output_buffer_batches_per_partition = output_buffer_batches;
169 Ok(self)
170 }
171
172 pub const fn with_prefetch_files_per_partition(mut self, prefetch_files: usize) -> Self {
174 self.prefetch_files_per_partition = prefetch_files;
175 self
176 }
177
178 pub fn with_parquet_metadata_size_hint_bytes(
180 mut self,
181 metadata_size_hint_bytes: Option<usize>,
182 ) -> Result<Self, DeltaReaderError> {
183 validate_optional_positive(
184 metadata_size_hint_bytes,
185 "parquet_metadata_size_hint_bytes_must_be_positive",
186 )?;
187 self.parquet_metadata_size_hint_bytes = metadata_size_hint_bytes;
188 Ok(self)
189 }
190
191 pub fn with_parquet_full_file_read_threshold_bytes(
193 mut self,
194 full_file_read_threshold_bytes: Option<usize>,
195 ) -> Result<Self, DeltaReaderError> {
196 validate_optional_positive(
197 full_file_read_threshold_bytes,
198 "parquet_full_file_read_threshold_bytes_must_be_positive",
199 )?;
200 self.parquet_full_file_read_threshold_bytes = full_file_read_threshold_bytes;
201 Ok(self)
202 }
203
204 pub(crate) fn resolved_max_concurrent_file_reads_per_scan(
205 &self,
206 target_partitions: usize,
207 ) -> usize {
208 self.max_concurrent_file_reads_per_scan.unwrap_or_else(|| {
209 target_partitions
210 .saturating_mul(self.max_concurrent_file_reads_per_partition)
211 .max(1)
212 })
213 }
214}
215
216impl Default for DeltaScanExecutionOptions {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222fn validate_positive(value: usize, reason: &'static str) -> Result<(), DeltaReaderError> {
223 if value == 0 {
224 return InvalidConfigurationSnafu { reason }.fail();
225 }
226 Ok(())
227}
228
229fn validate_optional_positive(
230 value: Option<usize>,
231 reason: &'static str,
232) -> Result<(), DeltaReaderError> {
233 if value == Some(0) {
234 return InvalidConfigurationSnafu { reason }.fail();
235 }
236 Ok(())
237}
238
239#[cfg(test)]
240mod tests {
241 use crate::DeltaReaderPhase;
242
243 use super::{
244 DeltaScanExecutionOptions, DeltaSnapshotSelection, DeltaStorageOptions,
245 ParquetReaderBackend,
246 };
247
248 #[test]
249 fn public_defaults_match_the_frozen_baseline() {
250 let options = DeltaScanExecutionOptions::new();
251
252 assert_eq!(
253 DeltaSnapshotSelection::default(),
254 DeltaSnapshotSelection::Latest
255 );
256 assert_eq!(
257 ParquetReaderBackend::default(),
258 ParquetReaderBackend::Direct
259 );
260 assert_eq!(DeltaScanExecutionOptions::default(), options);
261 assert_eq!(options.parquet_backend(), ParquetReaderBackend::Direct);
262 assert_eq!(options.max_concurrent_file_reads_per_scan(), None);
263 assert_eq!(options.max_concurrent_file_reads_per_partition(), 3);
264 assert_eq!(options.output_buffer_batches_per_partition(), 1);
265 assert_eq!(options.prefetch_files_per_partition(), 2);
266 assert_eq!(options.parquet_metadata_size_hint_bytes(), Some(65_536));
267 assert_eq!(options.parquet_full_file_read_threshold_bytes(), None);
268 assert_eq!(DeltaStorageOptions::default(), DeltaStorageOptions::new());
269 assert_eq!(
270 DeltaSnapshotSelection::Version(7),
271 DeltaSnapshotSelection::Version(7)
272 );
273 }
274
275 #[test]
276 fn builders_set_every_public_option() -> Result<(), Box<dyn std::error::Error>> {
277 let options = DeltaScanExecutionOptions::new()
278 .with_parquet_backend(ParquetReaderBackend::DeltaKernel)
279 .with_max_concurrent_file_reads_per_scan(Some(8))?
280 .with_max_concurrent_file_reads_per_partition(4)?
281 .with_output_buffer_batches_per_partition(2)?
282 .with_prefetch_files_per_partition(0)
283 .with_parquet_metadata_size_hint_bytes(None)?
284 .with_parquet_full_file_read_threshold_bytes(Some(1024))?;
285
286 assert_eq!(options.parquet_backend(), ParquetReaderBackend::DeltaKernel);
287 assert_eq!(options.max_concurrent_file_reads_per_scan(), Some(8));
288 assert_eq!(options.max_concurrent_file_reads_per_partition(), 4);
289 assert_eq!(options.output_buffer_batches_per_partition(), 2);
290 assert_eq!(options.prefetch_files_per_partition(), 0);
291 assert_eq!(options.parquet_metadata_size_hint_bytes(), None);
292 assert_eq!(options.parquet_full_file_read_threshold_bytes(), Some(1024));
293 Ok(())
294 }
295
296 #[test]
297 fn invalid_bounds_return_redacted_configuration_errors() {
298 let invalid = [
299 DeltaScanExecutionOptions::new().with_max_concurrent_file_reads_per_scan(Some(0)),
300 DeltaScanExecutionOptions::new().with_max_concurrent_file_reads_per_partition(0),
301 DeltaScanExecutionOptions::new().with_output_buffer_batches_per_partition(0),
302 DeltaScanExecutionOptions::new().with_parquet_metadata_size_hint_bytes(Some(0)),
303 DeltaScanExecutionOptions::new().with_parquet_full_file_read_threshold_bytes(Some(0)),
304 ];
305
306 for result in invalid {
307 let error = result.expect_err("invalid execution options must fail");
308 assert_eq!(error.phase(), DeltaReaderPhase::Configuration);
309 assert_eq!(error.code(), "invalid_configuration");
310 }
311 }
312
313 #[test]
314 fn independent_bounds_preserve_the_frozen_reader_behavior()
315 -> Result<(), Box<dyn std::error::Error>> {
316 let options = DeltaScanExecutionOptions::new()
317 .with_max_concurrent_file_reads_per_scan(Some(2))?
318 .with_prefetch_files_per_partition(4);
319
320 assert_eq!(options.max_concurrent_file_reads_per_scan(), Some(2));
321 assert_eq!(options.max_concurrent_file_reads_per_partition(), 3);
322 assert_eq!(options.prefetch_files_per_partition(), 4);
323 Ok(())
324 }
325
326 #[test]
327 fn scan_capacity_resolves_once_from_the_fixed_partition_target()
328 -> Result<(), Box<dyn std::error::Error>> {
329 let defaults = DeltaScanExecutionOptions::new();
330 assert_eq!(defaults.resolved_max_concurrent_file_reads_per_scan(4), 12);
331 assert_eq!(
332 defaults.resolved_max_concurrent_file_reads_per_scan(usize::MAX),
333 usize::MAX
334 );
335
336 let explicit = defaults.with_max_concurrent_file_reads_per_scan(Some(7))?;
337 assert_eq!(explicit.resolved_max_concurrent_file_reads_per_scan(4), 7);
338 Ok(())
339 }
340}