1use std::fmt;
2
3use snafu::Snafu;
4
5#[non_exhaustive]
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum DeltaReaderPhase {
9 Configuration,
11 TableUri,
13 Storage,
15 Snapshot,
17 Protocol,
19 Schema,
21 ScanPlanning,
23 DataFileRead,
25 DeletionVector,
27 Transform,
29 Execution,
31 DataFusion,
33}
34
35impl DeltaReaderPhase {
36 pub const fn as_str(self) -> &'static str {
38 match self {
39 Self::Configuration => "configuration",
40 Self::TableUri => "table_uri",
41 Self::Storage => "storage",
42 Self::Snapshot => "snapshot",
43 Self::Protocol => "protocol",
44 Self::Schema => "schema",
45 Self::ScanPlanning => "scan_planning",
46 Self::DataFileRead => "data_file_read",
47 Self::DeletionVector => "deletion_vector",
48 Self::Transform => "transform",
49 Self::Execution => "execution",
50 Self::DataFusion => "data_fusion",
51 }
52 }
53}
54
55#[non_exhaustive]
57#[derive(Snafu)]
58#[snafu(visibility(pub(crate)))]
59pub enum DeltaReaderError {
60 #[non_exhaustive]
62 #[snafu(display(
63 "delta reader error: phase=configuration error=invalid_configuration reason={reason}"
64 ))]
65 InvalidConfiguration {
66 reason: &'static str,
68 },
69 #[non_exhaustive]
71 #[snafu(display(
72 "delta reader error: phase=table_uri error=invalid_table_uri reason={reason}"
73 ))]
74 InvalidTableUri {
75 reason: &'static str,
77 },
78 #[non_exhaustive]
80 #[snafu(display(
81 "delta reader error: phase=storage error=storage_initialization reason={reason}"
82 ))]
83 StorageInitialization {
84 reason: &'static str,
86 #[snafu(source(from(exact)))]
88 source: Box<dyn std::error::Error + Send + Sync + 'static>,
89 },
90 #[non_exhaustive]
92 #[snafu(display("delta reader error: phase=snapshot error=snapshot_load reason={reason}"))]
93 SnapshotLoad {
94 reason: &'static str,
96 #[snafu(source(from(exact)))]
98 source: Box<dyn std::error::Error + Send + Sync + 'static>,
99 },
100 #[non_exhaustive]
102 #[snafu(display(
103 "delta reader error: phase=protocol error=unsupported_protocol reason={reason}"
104 ))]
105 UnsupportedProtocol {
106 reason: &'static str,
108 },
109 #[non_exhaustive]
111 #[snafu(display("delta reader error: phase=schema error=schema_conversion reason={reason}"))]
112 SchemaConversion {
113 reason: &'static str,
115 #[snafu(source(from(exact)))]
117 source: Box<dyn std::error::Error + Send + Sync + 'static>,
118 },
119 #[non_exhaustive]
121 #[snafu(display(
122 "delta reader error: phase=scan_planning error=invalid_projection reason={reason}"
123 ))]
124 InvalidProjection {
125 reason: &'static str,
127 },
128 #[non_exhaustive]
130 #[snafu(display(
131 "delta reader error: phase=scan_planning error=unsupported_predicate reason={reason}"
132 ))]
133 UnsupportedPredicate {
134 reason: &'static str,
136 },
137 #[non_exhaustive]
139 #[snafu(display(
140 "delta reader error: phase=scan_planning error=scan_planning reason={reason}"
141 ))]
142 ScanPlanning {
143 reason: &'static str,
145 #[snafu(source(from(exact)))]
147 source: Box<dyn std::error::Error + Send + Sync + 'static>,
148 },
149 #[non_exhaustive]
151 #[snafu(display(
152 "delta reader error: phase=scan_planning error=scan_partition_planning reason={reason}"
153 ))]
154 ScanPartitionPlanning {
155 reason: &'static str,
157 },
158 #[non_exhaustive]
160 #[snafu(display(
161 "delta reader error: phase=configuration error=unsupported_backend reason={reason}"
162 ))]
163 UnsupportedBackend {
164 reason: &'static str,
166 },
167 #[non_exhaustive]
169 #[snafu(display(
170 "delta reader error: phase=data_file_read error=data_file_read reason={reason}"
171 ))]
172 DataFileRead {
173 reason: &'static str,
175 #[snafu(source(from(exact)))]
177 source: Box<dyn std::error::Error + Send + Sync + 'static>,
178 },
179 #[non_exhaustive]
181 #[snafu(display(
182 "delta reader error: phase=deletion_vector error=deletion_vector_read reason={reason}"
183 ))]
184 DeletionVectorRead {
185 reason: &'static str,
187 #[snafu(source(from(exact)))]
189 source: Box<dyn std::error::Error + Send + Sync + 'static>,
190 },
191 #[non_exhaustive]
193 #[snafu(display(
194 "delta reader error: phase=transform error=physical_to_logical_transform reason={reason}"
195 ))]
196 PhysicalToLogicalTransform {
197 reason: &'static str,
199 #[snafu(source(from(exact)))]
201 source: Box<dyn std::error::Error + Send + Sync + 'static>,
202 },
203 #[non_exhaustive]
205 #[snafu(display("delta reader error: phase=execution error=cancelled reason={reason}"))]
206 Cancelled {
207 reason: &'static str,
209 },
210 #[cfg(feature = "datafusion")]
212 #[non_exhaustive]
213 #[snafu(display(
214 "delta reader error: phase=data_fusion error=data_fusion_adapter reason={reason}"
215 ))]
216 DataFusionAdapter {
217 reason: &'static str,
219 #[snafu(source(from(datafusion::common::DataFusionError, Box::new)))]
221 source: Box<datafusion::common::DataFusionError>,
222 },
223}
224
225impl DeltaReaderError {
226 pub const fn as_str(&self) -> &'static str {
228 match self {
229 Self::InvalidConfiguration { .. } => "invalid_configuration",
230 Self::InvalidTableUri { .. } => "invalid_table_uri",
231 Self::StorageInitialization { .. } => "storage_initialization",
232 Self::SnapshotLoad { .. } => "snapshot_load",
233 Self::UnsupportedProtocol { .. } => "unsupported_protocol",
234 Self::SchemaConversion { .. } => "schema_conversion",
235 Self::InvalidProjection { .. } => "invalid_projection",
236 Self::UnsupportedPredicate { .. } => "unsupported_predicate",
237 Self::ScanPlanning { .. } => "scan_planning",
238 Self::ScanPartitionPlanning { .. } => "scan_partition_planning",
239 Self::UnsupportedBackend { .. } => "unsupported_backend",
240 Self::DataFileRead { .. } => "data_file_read",
241 Self::DeletionVectorRead { .. } => "deletion_vector_read",
242 Self::PhysicalToLogicalTransform { .. } => "physical_to_logical_transform",
243 Self::Cancelled { .. } => "cancelled",
244 #[cfg(feature = "datafusion")]
245 Self::DataFusionAdapter { .. } => "data_fusion_adapter",
246 }
247 }
248
249 pub const fn phase(&self) -> DeltaReaderPhase {
251 match self {
252 Self::InvalidConfiguration { .. } => DeltaReaderPhase::Configuration,
253 Self::InvalidTableUri { .. } => DeltaReaderPhase::TableUri,
254 Self::StorageInitialization { .. } => DeltaReaderPhase::Storage,
255 Self::SnapshotLoad { .. } => DeltaReaderPhase::Snapshot,
256 Self::UnsupportedProtocol { .. } => DeltaReaderPhase::Protocol,
257 Self::SchemaConversion { .. } => DeltaReaderPhase::Schema,
258 Self::InvalidProjection { .. }
259 | Self::UnsupportedPredicate { .. }
260 | Self::ScanPlanning { .. }
261 | Self::ScanPartitionPlanning { .. } => DeltaReaderPhase::ScanPlanning,
262 Self::UnsupportedBackend { .. } => DeltaReaderPhase::Configuration,
263 Self::Cancelled { .. } => DeltaReaderPhase::Execution,
264 Self::DataFileRead { .. } => DeltaReaderPhase::DataFileRead,
265 Self::DeletionVectorRead { .. } => DeltaReaderPhase::DeletionVector,
266 Self::PhysicalToLogicalTransform { .. } => DeltaReaderPhase::Transform,
267 #[cfg(feature = "datafusion")]
268 Self::DataFusionAdapter { .. } => DeltaReaderPhase::DataFusion,
269 }
270 }
271}
272
273impl fmt::Debug for DeltaReaderError {
274 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
275 fmt::Display::fmt(self, formatter)
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use std::{error::Error as _, io};
282
283 use super::{DeltaReaderError, DeltaReaderPhase};
284
285 #[test]
286 fn phase_names_are_stable() {
287 let cases = [
288 (DeltaReaderPhase::Configuration, "configuration"),
289 (DeltaReaderPhase::TableUri, "table_uri"),
290 (DeltaReaderPhase::Storage, "storage"),
291 (DeltaReaderPhase::Snapshot, "snapshot"),
292 (DeltaReaderPhase::Protocol, "protocol"),
293 (DeltaReaderPhase::Schema, "schema"),
294 (DeltaReaderPhase::ScanPlanning, "scan_planning"),
295 (DeltaReaderPhase::DataFileRead, "data_file_read"),
296 (DeltaReaderPhase::DeletionVector, "deletion_vector"),
297 (DeltaReaderPhase::Transform, "transform"),
298 (DeltaReaderPhase::Execution, "execution"),
299 (DeltaReaderPhase::DataFusion, "data_fusion"),
300 ];
301
302 for (phase, expected) in cases {
303 assert_eq!(phase.as_str(), expected);
304 }
305 }
306
307 #[test]
308 fn variants_map_to_stable_accessors_and_sources() {
309 let errors = [
310 (
311 DeltaReaderError::InvalidConfiguration {
312 reason: "invalid_configuration",
313 },
314 "invalid_configuration",
315 DeltaReaderPhase::Configuration,
316 false,
317 ),
318 (
319 DeltaReaderError::InvalidTableUri {
320 reason: "invalid_table_uri",
321 },
322 "invalid_table_uri",
323 DeltaReaderPhase::TableUri,
324 false,
325 ),
326 (
327 DeltaReaderError::StorageInitialization {
328 reason: "storage_initialization",
329 source: dependency_source(),
330 },
331 "storage_initialization",
332 DeltaReaderPhase::Storage,
333 true,
334 ),
335 (
336 DeltaReaderError::SnapshotLoad {
337 reason: "snapshot_load",
338 source: dependency_source(),
339 },
340 "snapshot_load",
341 DeltaReaderPhase::Snapshot,
342 true,
343 ),
344 (
345 DeltaReaderError::UnsupportedProtocol {
346 reason: "unsupported_protocol",
347 },
348 "unsupported_protocol",
349 DeltaReaderPhase::Protocol,
350 false,
351 ),
352 (
353 DeltaReaderError::SchemaConversion {
354 reason: "schema_conversion",
355 source: dependency_source(),
356 },
357 "schema_conversion",
358 DeltaReaderPhase::Schema,
359 true,
360 ),
361 (
362 DeltaReaderError::InvalidProjection {
363 reason: "invalid_projection",
364 },
365 "invalid_projection",
366 DeltaReaderPhase::ScanPlanning,
367 false,
368 ),
369 (
370 DeltaReaderError::UnsupportedPredicate {
371 reason: "unsupported_predicate",
372 },
373 "unsupported_predicate",
374 DeltaReaderPhase::ScanPlanning,
375 false,
376 ),
377 (
378 DeltaReaderError::ScanPlanning {
379 reason: "scan_planning",
380 source: dependency_source(),
381 },
382 "scan_planning",
383 DeltaReaderPhase::ScanPlanning,
384 true,
385 ),
386 (
387 DeltaReaderError::ScanPartitionPlanning {
388 reason: "scan_partition_planning",
389 },
390 "scan_partition_planning",
391 DeltaReaderPhase::ScanPlanning,
392 false,
393 ),
394 (
395 DeltaReaderError::UnsupportedBackend {
396 reason: "unsupported_backend",
397 },
398 "unsupported_backend",
399 DeltaReaderPhase::Configuration,
400 false,
401 ),
402 (
403 DeltaReaderError::DataFileRead {
404 reason: "data_file_read",
405 source: dependency_source(),
406 },
407 "data_file_read",
408 DeltaReaderPhase::DataFileRead,
409 true,
410 ),
411 (
412 DeltaReaderError::DeletionVectorRead {
413 reason: "deletion_vector_read",
414 source: dependency_source(),
415 },
416 "deletion_vector_read",
417 DeltaReaderPhase::DeletionVector,
418 true,
419 ),
420 (
421 DeltaReaderError::PhysicalToLogicalTransform {
422 reason: "physical_to_logical_transform",
423 source: dependency_source(),
424 },
425 "physical_to_logical_transform",
426 DeltaReaderPhase::Transform,
427 true,
428 ),
429 (
430 DeltaReaderError::Cancelled {
431 reason: "cancelled",
432 },
433 "cancelled",
434 DeltaReaderPhase::Execution,
435 false,
436 ),
437 #[cfg(feature = "datafusion")]
438 (
439 DeltaReaderError::DataFusionAdapter {
440 reason: "data_fusion_adapter",
441 source: Box::new(datafusion::common::DataFusionError::Execution(
442 "sensitive dependency detail".into(),
443 )),
444 },
445 "data_fusion_adapter",
446 DeltaReaderPhase::DataFusion,
447 true,
448 ),
449 ];
450
451 for (error, name, phase, has_source) in errors {
452 assert_eq!(error.source().is_some(), has_source);
453 assert_eq!(error.as_str(), name);
454 assert_eq!(error.phase(), phase);
455 let display = error.to_string();
456 let debug = format!("{error:?}");
457 assert!(display.contains(&format!("phase={}", phase.as_str())));
458 assert!(display.contains(&format!("error={name}")));
459 assert!(!display.contains("sensitive dependency detail"));
460 assert!(!debug.contains("sensitive dependency detail"));
461 }
462 }
463
464 fn dependency_source() -> Box<dyn std::error::Error + Send + Sync + 'static> {
465 Box::new(io::Error::other("sensitive dependency detail"))
466 }
467
468 #[test]
469 fn boxed_source_preserves_its_concrete_type() {
470 let error = DeltaReaderError::DataFileRead {
471 reason: "data_file_read",
472 source: dependency_source(),
473 };
474
475 assert!(
476 error
477 .source()
478 .and_then(|source| source.downcast_ref::<io::Error>())
479 .is_some()
480 );
481 }
482
483 #[cfg(feature = "datafusion")]
484 #[test]
485 fn datafusion_source_preserves_its_boxed_type() {
486 let error = DeltaReaderError::DataFusionAdapter {
487 reason: "data_fusion_adapter",
488 source: Box::new(datafusion::common::DataFusionError::Execution(
489 "failure".into(),
490 )),
491 };
492
493 assert!(
494 error
495 .source()
496 .and_then(|source| {
497 source.downcast_ref::<Box<datafusion::common::DataFusionError>>()
498 })
499 .is_some()
500 );
501 }
502}