Skip to main content

delta_arrow_reader/
error.rs

1use std::fmt;
2
3use snafu::Snafu;
4
5/// Reader operation phase associated with an error.
6#[non_exhaustive]
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum DeltaReaderPhase {
9    /// Reader configuration validation.
10    Configuration,
11    /// Delta table path or URL parsing and normalization.
12    TableLocation,
13    /// Object-store initialization.
14    Storage,
15    /// Delta snapshot loading.
16    Snapshot,
17    /// Delta protocol validation.
18    Protocol,
19    /// Delta-to-Arrow schema conversion.
20    Schema,
21    /// Delta scan planning.
22    ScanPlanning,
23    /// Delta data-file reading.
24    DataFileRead,
25    /// Delta deletion-vector handling.
26    DeletionVector,
27    /// Physical-to-logical data transformation.
28    Transform,
29    /// Reader execution.
30    Execution,
31    /// Optional DataFusion integration.
32    DataFusion,
33}
34
35impl DeltaReaderPhase {
36    /// Returns the stable snake_case phase name.
37    pub const fn as_str(self) -> &'static str {
38        match self {
39            Self::Configuration => "configuration",
40            Self::TableLocation => "table_location",
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 => "datafusion",
51        }
52    }
53}
54
55/// Redacted failure returned by reader APIs.
56#[non_exhaustive]
57#[derive(Snafu)]
58#[snafu(visibility(pub(crate)))]
59pub enum DeltaReaderError {
60    /// Reader configuration is invalid.
61    #[non_exhaustive]
62    #[snafu(display(
63        "delta reader error: phase=configuration code=invalid_configuration reason={reason}"
64    ))]
65    InvalidConfiguration {
66        /// Fixed redacted reason category.
67        reason: &'static str,
68    },
69    /// The table path or URL is invalid.
70    #[non_exhaustive]
71    #[snafu(display(
72        "delta reader error: phase=table_location code=invalid_table_location reason={reason}"
73    ))]
74    InvalidTableLocation {
75        /// Fixed redacted reason category.
76        reason: &'static str,
77    },
78    /// Object-store initialization failed.
79    #[non_exhaustive]
80    #[snafu(display(
81        "delta reader error: phase=storage code=storage_initialization reason={reason}"
82    ))]
83    StorageInitialization {
84        /// Fixed redacted reason category.
85        reason: &'static str,
86        /// Underlying dependency failure.
87        #[snafu(source(from(exact)))]
88        source: Box<dyn std::error::Error + Send + Sync + 'static>,
89    },
90    /// Snapshot loading failed.
91    #[non_exhaustive]
92    #[snafu(display("delta reader error: phase=snapshot code=snapshot_load reason={reason}"))]
93    SnapshotLoad {
94        /// Fixed redacted reason category.
95        reason: &'static str,
96        /// Underlying dependency failure.
97        #[snafu(source(from(exact)))]
98        source: Box<dyn std::error::Error + Send + Sync + 'static>,
99    },
100    /// The table protocol is unsupported.
101    #[non_exhaustive]
102    #[snafu(display(
103        "delta reader error: phase=protocol code=unsupported_protocol reason={reason}"
104    ))]
105    UnsupportedProtocol {
106        /// Fixed redacted reason category.
107        reason: &'static str,
108    },
109    /// Delta-to-Arrow schema conversion failed.
110    #[non_exhaustive]
111    #[snafu(display("delta reader error: phase=schema code=schema_conversion reason={reason}"))]
112    SchemaConversion {
113        /// Fixed redacted reason category.
114        reason: &'static str,
115        /// Underlying dependency failure.
116        #[snafu(source(from(exact)))]
117        source: Box<dyn std::error::Error + Send + Sync + 'static>,
118    },
119    /// A requested projection is invalid.
120    #[non_exhaustive]
121    #[snafu(display(
122        "delta reader error: phase=scan_planning code=invalid_projection reason={reason}"
123    ))]
124    InvalidProjection {
125        /// Fixed redacted reason category.
126        reason: &'static str,
127    },
128    /// A requested predicate is unsupported.
129    #[non_exhaustive]
130    #[snafu(display(
131        "delta reader error: phase=scan_planning code=unsupported_predicate reason={reason}"
132    ))]
133    UnsupportedPredicate {
134        /// Fixed redacted reason category.
135        reason: &'static str,
136    },
137    /// Delta scan planning failed.
138    #[non_exhaustive]
139    #[snafu(display("delta reader error: phase=scan_planning code=scan_planning reason={reason}"))]
140    ScanPlanning {
141        /// Fixed redacted reason category.
142        reason: &'static str,
143        /// Underlying dependency failure.
144        #[snafu(source(from(exact)))]
145        source: Box<dyn std::error::Error + Send + Sync + 'static>,
146    },
147    /// Delta scan file tasks could not be grouped into partitions.
148    #[non_exhaustive]
149    #[snafu(display(
150        "delta reader error: phase=scan_planning code=scan_partition_planning reason={reason}"
151    ))]
152    ScanPartitionPlanning {
153        /// Fixed redacted reason category.
154        reason: &'static str,
155    },
156    /// A Delta data file could not be read.
157    #[non_exhaustive]
158    #[snafu(display(
159        "delta reader error: phase=data_file_read code=data_file_read reason={reason}"
160    ))]
161    DataFileRead {
162        /// Fixed redacted reason category.
163        reason: &'static str,
164        /// Underlying dependency failure.
165        #[snafu(source(from(exact)))]
166        source: Box<dyn std::error::Error + Send + Sync + 'static>,
167    },
168    /// A deletion vector could not be read.
169    #[non_exhaustive]
170    #[snafu(display(
171        "delta reader error: phase=deletion_vector code=deletion_vector_read reason={reason}"
172    ))]
173    DeletionVectorRead {
174        /// Fixed redacted reason category.
175        reason: &'static str,
176        /// Underlying dependency failure.
177        #[snafu(source(from(exact)))]
178        source: Box<dyn std::error::Error + Send + Sync + 'static>,
179    },
180    /// A physical-to-logical transform failed.
181    #[non_exhaustive]
182    #[snafu(display(
183        "delta reader error: phase=transform code=physical_to_logical_transform reason={reason}"
184    ))]
185    PhysicalToLogicalTransform {
186        /// Fixed redacted reason category.
187        reason: &'static str,
188        /// Underlying dependency failure.
189        #[snafu(source(from(exact)))]
190        source: Box<dyn std::error::Error + Send + Sync + 'static>,
191    },
192    /// Reader execution was cancelled.
193    #[non_exhaustive]
194    #[snafu(display("delta reader error: phase=execution code=cancelled reason={reason}"))]
195    Cancelled {
196        /// Fixed redacted reason category.
197        reason: &'static str,
198    },
199    /// Optional DataFusion integration failed.
200    #[cfg(feature = "datafusion")]
201    #[non_exhaustive]
202    #[snafu(display(
203        "delta reader error: phase=datafusion code=datafusion_adapter reason={reason}"
204    ))]
205    DataFusionAdapter {
206        /// Fixed redacted reason category.
207        reason: &'static str,
208        /// Underlying DataFusion failure.
209        #[snafu(source(from(datafusion::common::DataFusionError, Box::new)))]
210        source: Box<datafusion::common::DataFusionError>,
211    },
212}
213
214impl DeltaReaderError {
215    /// Returns the stable snake_case error code.
216    pub const fn code(&self) -> &'static str {
217        match self {
218            Self::InvalidConfiguration { .. } => "invalid_configuration",
219            Self::InvalidTableLocation { .. } => "invalid_table_location",
220            Self::StorageInitialization { .. } => "storage_initialization",
221            Self::SnapshotLoad { .. } => "snapshot_load",
222            Self::UnsupportedProtocol { .. } => "unsupported_protocol",
223            Self::SchemaConversion { .. } => "schema_conversion",
224            Self::InvalidProjection { .. } => "invalid_projection",
225            Self::UnsupportedPredicate { .. } => "unsupported_predicate",
226            Self::ScanPlanning { .. } => "scan_planning",
227            Self::ScanPartitionPlanning { .. } => "scan_partition_planning",
228            Self::DataFileRead { .. } => "data_file_read",
229            Self::DeletionVectorRead { .. } => "deletion_vector_read",
230            Self::PhysicalToLogicalTransform { .. } => "physical_to_logical_transform",
231            Self::Cancelled { .. } => "cancelled",
232            #[cfg(feature = "datafusion")]
233            Self::DataFusionAdapter { .. } => "datafusion_adapter",
234        }
235    }
236
237    /// Returns the reader phase that failed.
238    pub const fn phase(&self) -> DeltaReaderPhase {
239        match self {
240            Self::InvalidConfiguration { .. } => DeltaReaderPhase::Configuration,
241            Self::InvalidTableLocation { .. } => DeltaReaderPhase::TableLocation,
242            Self::StorageInitialization { .. } => DeltaReaderPhase::Storage,
243            Self::SnapshotLoad { .. } => DeltaReaderPhase::Snapshot,
244            Self::UnsupportedProtocol { .. } => DeltaReaderPhase::Protocol,
245            Self::SchemaConversion { .. } => DeltaReaderPhase::Schema,
246            Self::InvalidProjection { .. }
247            | Self::UnsupportedPredicate { .. }
248            | Self::ScanPlanning { .. }
249            | Self::ScanPartitionPlanning { .. } => DeltaReaderPhase::ScanPlanning,
250            Self::Cancelled { .. } => DeltaReaderPhase::Execution,
251            Self::DataFileRead { .. } => DeltaReaderPhase::DataFileRead,
252            Self::DeletionVectorRead { .. } => DeltaReaderPhase::DeletionVector,
253            Self::PhysicalToLogicalTransform { .. } => DeltaReaderPhase::Transform,
254            #[cfg(feature = "datafusion")]
255            Self::DataFusionAdapter { .. } => DeltaReaderPhase::DataFusion,
256        }
257    }
258}
259
260impl fmt::Debug for DeltaReaderError {
261    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
262        fmt::Display::fmt(self, formatter)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use std::{error::Error as _, io};
269
270    use super::{DeltaReaderError, DeltaReaderPhase};
271
272    #[test]
273    fn phase_names_are_stable() {
274        let cases = [
275            (DeltaReaderPhase::Configuration, "configuration"),
276            (DeltaReaderPhase::TableLocation, "table_location"),
277            (DeltaReaderPhase::Storage, "storage"),
278            (DeltaReaderPhase::Snapshot, "snapshot"),
279            (DeltaReaderPhase::Protocol, "protocol"),
280            (DeltaReaderPhase::Schema, "schema"),
281            (DeltaReaderPhase::ScanPlanning, "scan_planning"),
282            (DeltaReaderPhase::DataFileRead, "data_file_read"),
283            (DeltaReaderPhase::DeletionVector, "deletion_vector"),
284            (DeltaReaderPhase::Transform, "transform"),
285            (DeltaReaderPhase::Execution, "execution"),
286            (DeltaReaderPhase::DataFusion, "datafusion"),
287        ];
288
289        for (phase, expected) in cases {
290            assert_eq!(phase.as_str(), expected);
291        }
292    }
293
294    #[test]
295    fn variants_map_to_stable_accessors_and_sources() {
296        let errors = [
297            (
298                DeltaReaderError::InvalidConfiguration {
299                    reason: "invalid_configuration",
300                },
301                "invalid_configuration",
302                DeltaReaderPhase::Configuration,
303                false,
304            ),
305            (
306                DeltaReaderError::InvalidTableLocation {
307                    reason: "invalid_table_location",
308                },
309                "invalid_table_location",
310                DeltaReaderPhase::TableLocation,
311                false,
312            ),
313            (
314                DeltaReaderError::StorageInitialization {
315                    reason: "storage_initialization",
316                    source: dependency_source(),
317                },
318                "storage_initialization",
319                DeltaReaderPhase::Storage,
320                true,
321            ),
322            (
323                DeltaReaderError::SnapshotLoad {
324                    reason: "snapshot_load",
325                    source: dependency_source(),
326                },
327                "snapshot_load",
328                DeltaReaderPhase::Snapshot,
329                true,
330            ),
331            (
332                DeltaReaderError::UnsupportedProtocol {
333                    reason: "unsupported_protocol",
334                },
335                "unsupported_protocol",
336                DeltaReaderPhase::Protocol,
337                false,
338            ),
339            (
340                DeltaReaderError::SchemaConversion {
341                    reason: "schema_conversion",
342                    source: dependency_source(),
343                },
344                "schema_conversion",
345                DeltaReaderPhase::Schema,
346                true,
347            ),
348            (
349                DeltaReaderError::InvalidProjection {
350                    reason: "invalid_projection",
351                },
352                "invalid_projection",
353                DeltaReaderPhase::ScanPlanning,
354                false,
355            ),
356            (
357                DeltaReaderError::UnsupportedPredicate {
358                    reason: "unsupported_predicate",
359                },
360                "unsupported_predicate",
361                DeltaReaderPhase::ScanPlanning,
362                false,
363            ),
364            (
365                DeltaReaderError::ScanPlanning {
366                    reason: "scan_planning",
367                    source: dependency_source(),
368                },
369                "scan_planning",
370                DeltaReaderPhase::ScanPlanning,
371                true,
372            ),
373            (
374                DeltaReaderError::ScanPartitionPlanning {
375                    reason: "scan_partition_planning",
376                },
377                "scan_partition_planning",
378                DeltaReaderPhase::ScanPlanning,
379                false,
380            ),
381            (
382                DeltaReaderError::DataFileRead {
383                    reason: "data_file_read",
384                    source: dependency_source(),
385                },
386                "data_file_read",
387                DeltaReaderPhase::DataFileRead,
388                true,
389            ),
390            (
391                DeltaReaderError::DeletionVectorRead {
392                    reason: "deletion_vector_read",
393                    source: dependency_source(),
394                },
395                "deletion_vector_read",
396                DeltaReaderPhase::DeletionVector,
397                true,
398            ),
399            (
400                DeltaReaderError::PhysicalToLogicalTransform {
401                    reason: "physical_to_logical_transform",
402                    source: dependency_source(),
403                },
404                "physical_to_logical_transform",
405                DeltaReaderPhase::Transform,
406                true,
407            ),
408            (
409                DeltaReaderError::Cancelled {
410                    reason: "cancelled",
411                },
412                "cancelled",
413                DeltaReaderPhase::Execution,
414                false,
415            ),
416            #[cfg(feature = "datafusion")]
417            (
418                DeltaReaderError::DataFusionAdapter {
419                    reason: "datafusion_adapter",
420                    source: Box::new(datafusion::common::DataFusionError::Execution(
421                        "sensitive dependency detail".into(),
422                    )),
423                },
424                "datafusion_adapter",
425                DeltaReaderPhase::DataFusion,
426                true,
427            ),
428        ];
429
430        for (error, name, phase, has_source) in errors {
431            assert_eq!(error.source().is_some(), has_source);
432            assert_eq!(error.code(), name);
433            assert_eq!(error.phase(), phase);
434            let display = error.to_string();
435            let debug = format!("{error:?}");
436            assert!(display.contains(&format!("phase={}", phase.as_str())));
437            assert!(display.contains(&format!("code={name}")));
438            assert!(!display.contains("sensitive dependency detail"));
439            assert!(!debug.contains("sensitive dependency detail"));
440        }
441    }
442
443    fn dependency_source() -> Box<dyn std::error::Error + Send + Sync + 'static> {
444        Box::new(io::Error::other("sensitive dependency detail"))
445    }
446
447    #[test]
448    fn boxed_source_preserves_its_concrete_type() {
449        let error = DeltaReaderError::DataFileRead {
450            reason: "data_file_read",
451            source: dependency_source(),
452        };
453
454        assert!(
455            error
456                .source()
457                .and_then(|source| source.downcast_ref::<io::Error>())
458                .is_some()
459        );
460    }
461
462    #[cfg(feature = "datafusion")]
463    #[test]
464    fn datafusion_source_preserves_its_boxed_type() {
465        let error = DeltaReaderError::DataFusionAdapter {
466            reason: "datafusion_adapter",
467            source: Box::new(datafusion::common::DataFusionError::Execution(
468                "failure".into(),
469            )),
470        };
471
472        assert!(
473            error
474                .source()
475                .and_then(|source| {
476                    source.downcast_ref::<Box<datafusion::common::DataFusionError>>()
477                })
478                .is_some()
479        );
480    }
481}