1use std::collections::BTreeMap;
2
3use crate::{DeltaReaderError, error::InvalidConfigurationSnafu};
4
5const DEFAULT_MAX_CONCURRENT_FILE_READS_PER_PARTITION: usize = 3;
6const DEFAULT_OUTPUT_BUFFER_CAPACITY_PER_PARTITION: usize = 1;
7const DEFAULT_NATIVE_ASYNC_PREFETCH_FILE_COUNT_PER_PARTITION: usize = 2;
8const DEFAULT_PARQUET_METADATA_SIZE_HINT: usize = 64 * 1024;
9
10pub type DeltaStorageOptions = BTreeMap<String, String>;
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub enum DeltaSnapshotSelection {
16 #[default]
18 Latest,
19 Version(u64),
21}
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub enum DeltaReaderBackend {
26 OfficialKernel,
28 #[default]
30 NativeAsync,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct DeltaReaderExecutionOptions {
36 reader_backend: DeltaReaderBackend,
37 max_concurrent_file_reads_per_scan: Option<usize>,
38 max_concurrent_file_reads_per_partition: usize,
39 output_buffer_capacity_per_partition: usize,
40 native_async_prefetch_file_count_per_partition: usize,
41 parquet_metadata_size_hint: Option<usize>,
42 parquet_full_file_read_threshold: Option<usize>,
43}
44
45impl DeltaReaderExecutionOptions {
46 pub const fn new() -> Self {
48 Self {
49 reader_backend: DeltaReaderBackend::NativeAsync,
50 max_concurrent_file_reads_per_scan: None,
51 max_concurrent_file_reads_per_partition:
52 DEFAULT_MAX_CONCURRENT_FILE_READS_PER_PARTITION,
53 output_buffer_capacity_per_partition: DEFAULT_OUTPUT_BUFFER_CAPACITY_PER_PARTITION,
54 native_async_prefetch_file_count_per_partition:
55 DEFAULT_NATIVE_ASYNC_PREFETCH_FILE_COUNT_PER_PARTITION,
56 parquet_metadata_size_hint: Some(DEFAULT_PARQUET_METADATA_SIZE_HINT),
57 parquet_full_file_read_threshold: None,
58 }
59 }
60
61 pub const fn reader_backend(&self) -> DeltaReaderBackend {
63 self.reader_backend
64 }
65
66 pub const fn max_concurrent_file_reads_per_scan(&self) -> Option<usize> {
68 self.max_concurrent_file_reads_per_scan
69 }
70
71 pub const fn max_concurrent_file_reads_per_partition(&self) -> usize {
73 self.max_concurrent_file_reads_per_partition
74 }
75
76 pub const fn output_buffer_capacity_per_partition(&self) -> usize {
78 self.output_buffer_capacity_per_partition
79 }
80
81 pub const fn native_async_prefetch_file_count_per_partition(&self) -> usize {
83 self.native_async_prefetch_file_count_per_partition
84 }
85
86 pub const fn parquet_metadata_size_hint(&self) -> Option<usize> {
88 self.parquet_metadata_size_hint
89 }
90
91 pub const fn parquet_full_file_read_threshold(&self) -> Option<usize> {
93 self.parquet_full_file_read_threshold
94 }
95
96 pub fn with_reader_backend(
98 mut self,
99 value: DeltaReaderBackend,
100 ) -> Result<Self, DeltaReaderError> {
101 self.reader_backend = value;
102 self.validate()?;
103 Ok(self)
104 }
105
106 pub fn with_max_concurrent_file_reads_per_scan(
108 mut self,
109 value: Option<usize>,
110 ) -> Result<Self, DeltaReaderError> {
111 self.max_concurrent_file_reads_per_scan = value;
112 self.validate()?;
113 Ok(self)
114 }
115
116 pub fn with_max_concurrent_file_reads_per_partition(
118 mut self,
119 value: usize,
120 ) -> Result<Self, DeltaReaderError> {
121 self.max_concurrent_file_reads_per_partition = value;
122 self.validate()?;
123 Ok(self)
124 }
125
126 pub fn with_output_buffer_capacity_per_partition(
128 mut self,
129 value: usize,
130 ) -> Result<Self, DeltaReaderError> {
131 self.output_buffer_capacity_per_partition = value;
132 self.validate()?;
133 Ok(self)
134 }
135
136 pub fn with_native_async_prefetch_file_count_per_partition(
138 mut self,
139 value: usize,
140 ) -> Result<Self, DeltaReaderError> {
141 self.native_async_prefetch_file_count_per_partition = value;
142 self.validate()?;
143 Ok(self)
144 }
145
146 pub fn with_parquet_metadata_size_hint(
148 mut self,
149 value: Option<usize>,
150 ) -> Result<Self, DeltaReaderError> {
151 self.parquet_metadata_size_hint = value;
152 self.validate()?;
153 Ok(self)
154 }
155
156 pub fn with_parquet_full_file_read_threshold(
158 mut self,
159 value: Option<usize>,
160 ) -> Result<Self, DeltaReaderError> {
161 self.parquet_full_file_read_threshold = value;
162 self.validate()?;
163 Ok(self)
164 }
165
166 pub fn validate(&self) -> Result<(), DeltaReaderError> {
168 validate_optional_positive(
169 self.max_concurrent_file_reads_per_scan,
170 "max_concurrent_file_reads_per_scan_must_be_positive",
171 )?;
172 validate_positive(
173 self.max_concurrent_file_reads_per_partition,
174 "max_concurrent_file_reads_per_partition_must_be_positive",
175 )?;
176 validate_positive(
177 self.output_buffer_capacity_per_partition,
178 "output_buffer_capacity_per_partition_must_be_positive",
179 )?;
180 validate_optional_positive(
181 self.parquet_metadata_size_hint,
182 "parquet_metadata_size_hint_must_be_positive",
183 )?;
184 validate_optional_positive(
185 self.parquet_full_file_read_threshold,
186 "parquet_full_file_read_threshold_must_be_positive",
187 )?;
188
189 if self
190 .max_concurrent_file_reads_per_scan
191 .is_some_and(|scan_limit| self.max_concurrent_file_reads_per_partition > scan_limit)
192 {
193 return InvalidConfigurationSnafu {
194 reason: "partition_file_read_limit_exceeds_scan_limit",
195 }
196 .fail();
197 }
198
199 if self.native_async_prefetch_file_count_per_partition
200 > self.max_concurrent_file_reads_per_partition
201 {
202 return InvalidConfigurationSnafu {
203 reason: "native_async_prefetch_exceeds_partition_file_read_limit",
204 }
205 .fail();
206 }
207
208 Ok(())
209 }
210
211 pub(crate) fn resolved_max_concurrent_file_reads_per_scan(
212 &self,
213 target_partitions: usize,
214 ) -> usize {
215 self.max_concurrent_file_reads_per_scan.unwrap_or_else(|| {
216 target_partitions
217 .saturating_mul(self.max_concurrent_file_reads_per_partition)
218 .max(1)
219 })
220 }
221}
222
223impl Default for DeltaReaderExecutionOptions {
224 fn default() -> Self {
225 Self::new()
226 }
227}
228
229fn validate_positive(value: usize, reason: &'static str) -> Result<(), DeltaReaderError> {
230 if value == 0 {
231 return InvalidConfigurationSnafu { reason }.fail();
232 }
233 Ok(())
234}
235
236fn validate_optional_positive(
237 value: Option<usize>,
238 reason: &'static str,
239) -> Result<(), DeltaReaderError> {
240 if value == Some(0) {
241 return InvalidConfigurationSnafu { reason }.fail();
242 }
243 Ok(())
244}
245
246#[cfg(test)]
247mod tests {
248 use crate::DeltaReaderPhase;
249
250 use super::{
251 DeltaReaderBackend, DeltaReaderExecutionOptions, DeltaSnapshotSelection,
252 DeltaStorageOptions,
253 };
254
255 #[test]
256 fn public_defaults_match_the_frozen_baseline() {
257 let options = DeltaReaderExecutionOptions::new();
258
259 assert_eq!(
260 DeltaSnapshotSelection::default(),
261 DeltaSnapshotSelection::Latest
262 );
263 assert_eq!(
264 DeltaReaderBackend::default(),
265 DeltaReaderBackend::NativeAsync
266 );
267 assert_eq!(DeltaReaderExecutionOptions::default(), options);
268 assert_eq!(options.reader_backend(), DeltaReaderBackend::NativeAsync);
269 assert_eq!(options.max_concurrent_file_reads_per_scan(), None);
270 assert_eq!(options.max_concurrent_file_reads_per_partition(), 3);
271 assert_eq!(options.output_buffer_capacity_per_partition(), 1);
272 assert_eq!(options.native_async_prefetch_file_count_per_partition(), 2);
273 assert_eq!(options.parquet_metadata_size_hint(), Some(65_536));
274 assert_eq!(options.parquet_full_file_read_threshold(), None);
275 assert_eq!(DeltaStorageOptions::default(), DeltaStorageOptions::new());
276 assert_eq!(
277 DeltaSnapshotSelection::Version(7),
278 DeltaSnapshotSelection::Version(7)
279 );
280 }
281
282 #[test]
283 fn builders_set_every_public_option() -> Result<(), Box<dyn std::error::Error>> {
284 let options = DeltaReaderExecutionOptions::new()
285 .with_reader_backend(DeltaReaderBackend::OfficialKernel)?
286 .with_max_concurrent_file_reads_per_scan(Some(8))?
287 .with_max_concurrent_file_reads_per_partition(4)?
288 .with_output_buffer_capacity_per_partition(2)?
289 .with_native_async_prefetch_file_count_per_partition(0)?
290 .with_parquet_metadata_size_hint(None)?
291 .with_parquet_full_file_read_threshold(Some(1024))?;
292
293 assert_eq!(options.reader_backend(), DeltaReaderBackend::OfficialKernel);
294 assert_eq!(options.max_concurrent_file_reads_per_scan(), Some(8));
295 assert_eq!(options.max_concurrent_file_reads_per_partition(), 4);
296 assert_eq!(options.output_buffer_capacity_per_partition(), 2);
297 assert_eq!(options.native_async_prefetch_file_count_per_partition(), 0);
298 assert_eq!(options.parquet_metadata_size_hint(), None);
299 assert_eq!(options.parquet_full_file_read_threshold(), Some(1024));
300 Ok(())
301 }
302
303 #[test]
304 fn invalid_bounds_return_redacted_configuration_errors() {
305 let invalid = [
306 DeltaReaderExecutionOptions::new().with_max_concurrent_file_reads_per_scan(Some(0)),
307 DeltaReaderExecutionOptions::new().with_max_concurrent_file_reads_per_partition(0),
308 DeltaReaderExecutionOptions::new().with_output_buffer_capacity_per_partition(0),
309 DeltaReaderExecutionOptions::new().with_parquet_metadata_size_hint(Some(0)),
310 DeltaReaderExecutionOptions::new().with_parquet_full_file_read_threshold(Some(0)),
311 DeltaReaderExecutionOptions::new().with_max_concurrent_file_reads_per_scan(Some(2)),
312 DeltaReaderExecutionOptions::new()
313 .with_native_async_prefetch_file_count_per_partition(4),
314 ];
315
316 for result in invalid {
317 let error = result.expect_err("invalid execution options must fail");
318 assert_eq!(error.phase(), DeltaReaderPhase::Configuration);
319 assert_eq!(error.as_str(), "invalid_configuration");
320 }
321 }
322
323 #[test]
324 fn scan_capacity_resolves_once_from_the_fixed_partition_target()
325 -> Result<(), Box<dyn std::error::Error>> {
326 let defaults = DeltaReaderExecutionOptions::new();
327 assert_eq!(defaults.resolved_max_concurrent_file_reads_per_scan(4), 12);
328 assert_eq!(
329 defaults.resolved_max_concurrent_file_reads_per_scan(usize::MAX),
330 usize::MAX
331 );
332
333 let explicit = defaults.with_max_concurrent_file_reads_per_scan(Some(7))?;
334 assert_eq!(explicit.resolved_max_concurrent_file_reads_per_scan(4), 7);
335 Ok(())
336 }
337}