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 URI parsing and normalization.
12    TableUri,
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::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/// 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 error=invalid_configuration reason={reason}"
64    ))]
65    InvalidConfiguration {
66        /// Fixed redacted reason category.
67        reason: &'static str,
68    },
69    /// The table URI is invalid.
70    #[non_exhaustive]
71    #[snafu(display(
72        "delta reader error: phase=table_uri error=invalid_table_uri reason={reason}"
73    ))]
74    InvalidTableUri {
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 error=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 error=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 error=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 error=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 error=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 error=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(
140        "delta reader error: phase=scan_planning error=scan_planning reason={reason}"
141    ))]
142    ScanPlanning {
143        /// Fixed redacted reason category.
144        reason: &'static str,
145        /// Underlying dependency failure.
146        #[snafu(source(from(exact)))]
147        source: Box<dyn std::error::Error + Send + Sync + 'static>,
148    },
149    /// Delta scan file tasks could not be grouped into partitions.
150    #[non_exhaustive]
151    #[snafu(display(
152        "delta reader error: phase=scan_planning error=scan_partition_planning reason={reason}"
153    ))]
154    ScanPartitionPlanning {
155        /// Fixed redacted reason category.
156        reason: &'static str,
157    },
158    /// The requested reader backend is unavailable.
159    #[non_exhaustive]
160    #[snafu(display(
161        "delta reader error: phase=configuration error=unsupported_backend reason={reason}"
162    ))]
163    UnsupportedBackend {
164        /// Fixed redacted reason category.
165        reason: &'static str,
166    },
167    /// A Delta data file could not be read.
168    #[non_exhaustive]
169    #[snafu(display(
170        "delta reader error: phase=data_file_read error=data_file_read reason={reason}"
171    ))]
172    DataFileRead {
173        /// Fixed redacted reason category.
174        reason: &'static str,
175        /// Underlying dependency failure.
176        #[snafu(source(from(exact)))]
177        source: Box<dyn std::error::Error + Send + Sync + 'static>,
178    },
179    /// A deletion vector could not be read.
180    #[non_exhaustive]
181    #[snafu(display(
182        "delta reader error: phase=deletion_vector error=deletion_vector_read reason={reason}"
183    ))]
184    DeletionVectorRead {
185        /// Fixed redacted reason category.
186        reason: &'static str,
187        /// Underlying dependency failure.
188        #[snafu(source(from(exact)))]
189        source: Box<dyn std::error::Error + Send + Sync + 'static>,
190    },
191    /// A physical-to-logical transform failed.
192    #[non_exhaustive]
193    #[snafu(display(
194        "delta reader error: phase=transform error=physical_to_logical_transform reason={reason}"
195    ))]
196    PhysicalToLogicalTransform {
197        /// Fixed redacted reason category.
198        reason: &'static str,
199        /// Underlying dependency failure.
200        #[snafu(source(from(exact)))]
201        source: Box<dyn std::error::Error + Send + Sync + 'static>,
202    },
203    /// Reader execution was cancelled.
204    #[non_exhaustive]
205    #[snafu(display("delta reader error: phase=execution error=cancelled reason={reason}"))]
206    Cancelled {
207        /// Fixed redacted reason category.
208        reason: &'static str,
209    },
210    /// Optional DataFusion integration failed.
211    #[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        /// Fixed redacted reason category.
218        reason: &'static str,
219        /// Underlying DataFusion failure.
220        #[snafu(source(from(datafusion::common::DataFusionError, Box::new)))]
221        source: Box<datafusion::common::DataFusionError>,
222    },
223}
224
225impl DeltaReaderError {
226    /// Returns the stable snake_case error variant name.
227    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    /// Returns the reader phase that failed.
250    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}