Skip to main content

hdbconnect_arrow/
error.rs

1//! Error hierarchy for hdbconnect-arrow.
2//!
3//! Follows the "canonical error struct" pattern from Microsoft Rust Guidelines.
4//! Exposes `is_xxx()` methods rather than internal `ErrorKind` for future-proofing.
5
6use thiserror::Error;
7
8/// Root error type for hdbconnect-arrow crate.
9///
10/// This error type captures all possible failure modes during HANA to Arrow
11/// conversion. Exposes predicate methods (`is_xxx()`) for error classification
12/// without exposing internals.
13///
14/// # Example
15///
16/// ```rust,ignore
17/// use hdbconnect_arrow::ArrowConversionError;
18///
19/// fn handle_error(err: ArrowConversionError) {
20///     if err.is_unsupported_type() {
21///         eprintln!("Unsupported HANA type encountered");
22///     } else if err.is_schema_mismatch() {
23///         eprintln!("Schema mismatch detected");
24///     }
25/// }
26/// ```
27#[derive(Error, Debug)]
28#[error("{kind}")]
29pub struct ArrowConversionError {
30    kind: ErrorKind,
31}
32
33/// Internal error classification.
34///
35/// This enum is `pub(crate)` to allow adding variants without breaking changes.
36/// External code should use the `is_xxx()` predicate methods instead.
37#[derive(Error, Debug)]
38#[non_exhaustive]
39pub(crate) enum ErrorKind {
40    /// A HANA type that cannot be mapped to Arrow.
41    #[error("unsupported HANA type: {type_id:?}")]
42    UnsupportedType { type_id: i16 },
43
44    /// Column count mismatch between expected and actual.
45    #[error("schema mismatch: expected {expected} columns, got {actual}")]
46    SchemaMismatch { expected: usize, actual: usize },
47
48    /// Value conversion failure for a specific column.
49    #[error("value conversion failed for column '{column}': {message}")]
50    ValueConversion {
51        column: String,
52        message: String,
53        #[source]
54        source: Option<Box<dyn std::error::Error + Send + Sync>>,
55    },
56
57    /// Decimal value exceeds Arrow Decimal128 capacity.
58    #[error("decimal overflow: precision {precision}, scale {scale}")]
59    DecimalOverflow { precision: u8, scale: i8 },
60
61    /// Error from Arrow library operations.
62    #[error("arrow error")]
63    Arrow(
64        #[source]
65        #[from]
66        arrow_schema::ArrowError,
67    ),
68
69    /// Error from hdbconnect library.
70    #[error("hdbconnect error: {message}")]
71    Hdbconnect {
72        message: String,
73        #[source]
74        source: Option<Box<dyn std::error::Error + Send + Sync>>,
75    },
76
77    /// Error during LOB streaming operations.
78    #[error("LOB streaming error: {message}")]
79    LobStreaming { message: String },
80
81    /// Invalid precision value for DECIMAL type.
82    #[error("invalid precision: {0}")]
83    InvalidPrecision(String),
84
85    /// Invalid scale value for DECIMAL type.
86    #[error("invalid scale: {0}")]
87    InvalidScale(String),
88}
89
90impl ArrowConversionError {
91    // ═══════════════════════════════════════════════════════════════════════
92    // Constructors
93    // ═══════════════════════════════════════════════════════════════════════
94
95    /// Create error for unsupported HANA type.
96    #[must_use]
97    pub const fn unsupported_type(type_id: i16) -> Self {
98        Self {
99            kind: ErrorKind::UnsupportedType { type_id },
100        }
101    }
102
103    /// Create error for schema mismatch.
104    #[must_use]
105    pub const fn schema_mismatch(expected: usize, actual: usize) -> Self {
106        Self {
107            kind: ErrorKind::SchemaMismatch { expected, actual },
108        }
109    }
110
111    /// Create error for value conversion failure.
112    #[must_use]
113    pub fn value_conversion(column: impl Into<String>, message: impl Into<String>) -> Self {
114        Self {
115            kind: ErrorKind::ValueConversion {
116                column: column.into(),
117                message: message.into(),
118                source: None,
119            },
120        }
121    }
122
123    /// Create error for value conversion failure with source error.
124    #[must_use]
125    pub fn value_conversion_with_source<E>(
126        column: impl Into<String>,
127        message: impl Into<String>,
128        source: E,
129    ) -> Self
130    where
131        E: std::error::Error + Send + Sync + 'static,
132    {
133        Self {
134            kind: ErrorKind::ValueConversion {
135                column: column.into(),
136                message: message.into(),
137                source: Some(Box::new(source)),
138            },
139        }
140    }
141
142    /// Create error for decimal overflow.
143    #[must_use]
144    pub const fn decimal_overflow(precision: u8, scale: i8) -> Self {
145        Self {
146            kind: ErrorKind::DecimalOverflow { precision, scale },
147        }
148    }
149
150    /// Create error for LOB streaming failure.
151    #[must_use]
152    pub fn lob_streaming(message: impl Into<String>) -> Self {
153        Self {
154            kind: ErrorKind::LobStreaming {
155                message: message.into(),
156            },
157        }
158    }
159
160    /// Create error for invalid precision.
161    #[must_use]
162    pub fn invalid_precision(message: impl Into<String>) -> Self {
163        Self {
164            kind: ErrorKind::InvalidPrecision(message.into()),
165        }
166    }
167
168    /// Create error for invalid scale.
169    #[must_use]
170    pub fn invalid_scale(message: impl Into<String>) -> Self {
171        Self {
172            kind: ErrorKind::InvalidScale(message.into()),
173        }
174    }
175
176    // ═══════════════════════════════════════════════════════════════════════
177    // Predicate Methods (is_xxx)
178    // ═══════════════════════════════════════════════════════════════════════
179
180    /// Returns true if this is an unsupported type error.
181    #[must_use]
182    pub const fn is_unsupported_type(&self) -> bool {
183        matches!(self.kind, ErrorKind::UnsupportedType { .. })
184    }
185
186    /// Returns true if this is a schema mismatch error.
187    #[must_use]
188    pub const fn is_schema_mismatch(&self) -> bool {
189        matches!(self.kind, ErrorKind::SchemaMismatch { .. })
190    }
191
192    /// Returns true if this is a value conversion error.
193    #[must_use]
194    pub const fn is_value_conversion(&self) -> bool {
195        matches!(self.kind, ErrorKind::ValueConversion { .. })
196    }
197
198    /// Returns true if this is a decimal overflow error.
199    #[must_use]
200    pub const fn is_decimal_overflow(&self) -> bool {
201        matches!(self.kind, ErrorKind::DecimalOverflow { .. })
202    }
203
204    /// Returns true if this is an Arrow library error.
205    #[must_use]
206    pub const fn is_arrow_error(&self) -> bool {
207        matches!(self.kind, ErrorKind::Arrow(_))
208    }
209
210    /// Returns true if this is an hdbconnect error.
211    #[must_use]
212    pub const fn is_hdbconnect_error(&self) -> bool {
213        matches!(self.kind, ErrorKind::Hdbconnect { .. })
214    }
215
216    /// Returns true if this is a LOB streaming error.
217    #[must_use]
218    pub const fn is_lob_streaming(&self) -> bool {
219        matches!(self.kind, ErrorKind::LobStreaming { .. })
220    }
221
222    /// Returns true if this is an invalid precision error.
223    #[must_use]
224    pub const fn is_invalid_precision(&self) -> bool {
225        matches!(self.kind, ErrorKind::InvalidPrecision(_))
226    }
227
228    /// Returns true if this is an invalid scale error.
229    #[must_use]
230    pub const fn is_invalid_scale(&self) -> bool {
231        matches!(self.kind, ErrorKind::InvalidScale(_))
232    }
233
234    // ═══════════════════════════════════════════════════════════════════════
235    // Error Classification Methods
236    // ═══════════════════════════════════════════════════════════════════════
237
238    /// Returns true if this error is potentially recoverable.
239    ///
240    /// Recoverable errors are typically transient issues that might
241    /// succeed if retried (e.g., network timeouts, temporary failures).
242    ///
243    /// Non-recoverable errors indicate permanent failures like schema
244    /// mismatches, unsupported types, or data corruption.
245    ///
246    /// # Example
247    ///
248    /// ```rust,ignore
249    /// fn process_with_retry<T>(f: impl Fn() -> Result<T>) -> Result<T> {
250    ///     for _ in 0..3 {
251    ///         match f() {
252    ///             Ok(v) => return Ok(v),
253    ///             Err(e) if e.is_recoverable() => continue,
254    ///             Err(e) => return Err(e),
255    ///         }
256    ///     }
257    ///     f() // Final attempt
258    /// }
259    /// ```
260    #[must_use]
261    #[allow(clippy::match_same_arms)]
262    pub const fn is_recoverable(&self) -> bool {
263        match &self.kind {
264            // Configuration/data errors - not recoverable
265            ErrorKind::UnsupportedType { .. } => false,
266            ErrorKind::SchemaMismatch { .. } => false,
267            ErrorKind::ValueConversion { .. } => false,
268            ErrorKind::DecimalOverflow { .. } => false,
269            ErrorKind::InvalidPrecision(_) => false,
270            ErrorKind::InvalidScale(_) => false,
271
272            // Arrow errors - generally not recoverable
273            ErrorKind::Arrow(_) => false,
274
275            // LOB streaming might be recoverable (network issues)
276            ErrorKind::LobStreaming { .. } => true,
277
278            // HANA errors need inspection - some might be transient
279            // Default to recoverable to allow retry logic
280            ErrorKind::Hdbconnect { .. } => true,
281        }
282    }
283
284    /// Returns true if this error is a configuration error.
285    ///
286    /// Configuration errors indicate incorrect setup that won't
287    /// be fixed by retrying.
288    #[must_use]
289    pub const fn is_configuration_error(&self) -> bool {
290        matches!(
291            &self.kind,
292            ErrorKind::UnsupportedType { .. }
293                | ErrorKind::SchemaMismatch { .. }
294                | ErrorKind::InvalidPrecision(_)
295                | ErrorKind::InvalidScale(_)
296        )
297    }
298
299    /// Returns true if this error is a data error.
300    ///
301    /// Data errors indicate issues with the data being processed.
302    #[must_use]
303    pub const fn is_data_error(&self) -> bool {
304        matches!(
305            &self.kind,
306            ErrorKind::ValueConversion { .. } | ErrorKind::DecimalOverflow { .. }
307        )
308    }
309}
310
311impl From<hdbconnect::HdbError> for ArrowConversionError {
312    fn from(err: hdbconnect::HdbError) -> Self {
313        Self {
314            kind: ErrorKind::Hdbconnect {
315                message: err.to_string(),
316                source: Some(Box::new(err)),
317            },
318        }
319    }
320}
321
322impl From<arrow_schema::ArrowError> for ArrowConversionError {
323    fn from(err: arrow_schema::ArrowError) -> Self {
324        Self {
325            kind: ErrorKind::Arrow(err),
326        }
327    }
328}
329
330/// Result type alias for Arrow conversion operations.
331pub type Result<T> = std::result::Result<T, ArrowConversionError>;
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    // ═══════════════════════════════════════════════════════════════════════════
338    // Constructor Tests
339    // ═══════════════════════════════════════════════════════════════════════════
340
341    #[test]
342    fn test_unsupported_type_creation() {
343        let err = ArrowConversionError::unsupported_type(42);
344        assert!(err.is_unsupported_type());
345        assert!(!err.is_schema_mismatch());
346        assert!(!err.is_value_conversion());
347        assert!(!err.is_decimal_overflow());
348        assert!(!err.is_arrow_error());
349        assert!(!err.is_hdbconnect_error());
350        assert!(!err.is_lob_streaming());
351        assert!(!err.is_invalid_precision());
352        assert!(!err.is_invalid_scale());
353    }
354
355    #[test]
356    fn test_schema_mismatch_creation() {
357        let err = ArrowConversionError::schema_mismatch(5, 3);
358        assert!(err.is_schema_mismatch());
359        assert!(!err.is_unsupported_type());
360        assert!(err.to_string().contains("expected 5 columns, got 3"));
361    }
362
363    #[test]
364    fn test_value_conversion_creation() {
365        let err = ArrowConversionError::value_conversion("col1", "invalid integer");
366        assert!(err.is_value_conversion());
367        assert!(!err.is_unsupported_type());
368        assert!(err.to_string().contains("col1"));
369        assert!(err.to_string().contains("invalid integer"));
370    }
371
372    #[test]
373    fn test_decimal_overflow_creation() {
374        let err = ArrowConversionError::decimal_overflow(38, 10);
375        assert!(err.is_decimal_overflow());
376        assert!(!err.is_unsupported_type());
377        assert!(err.to_string().contains("precision 38"));
378        assert!(err.to_string().contains("scale 10"));
379    }
380
381    #[test]
382    fn test_lob_streaming_creation() {
383        let err = ArrowConversionError::lob_streaming("connection lost");
384        assert!(err.is_lob_streaming());
385        assert!(!err.is_unsupported_type());
386        assert!(err.to_string().contains("LOB streaming error"));
387        assert!(err.to_string().contains("connection lost"));
388    }
389
390    #[test]
391    fn test_invalid_precision_creation() {
392        let err = ArrowConversionError::invalid_precision("precision must be positive");
393        assert!(err.is_invalid_precision());
394        assert!(!err.is_unsupported_type());
395        assert!(err.to_string().contains("invalid precision"));
396    }
397
398    #[test]
399    fn test_invalid_scale_creation() {
400        let err = ArrowConversionError::invalid_scale("scale exceeds precision");
401        assert!(err.is_invalid_scale());
402        assert!(!err.is_unsupported_type());
403        assert!(err.to_string().contains("invalid scale"));
404    }
405
406    // ═══════════════════════════════════════════════════════════════════════════
407    // From Conversion Tests
408    // ═══════════════════════════════════════════════════════════════════════════
409
410    #[test]
411    fn test_from_arrow_error() {
412        let arrow_err = arrow_schema::ArrowError::SchemaError("test error".to_string());
413        let err: ArrowConversionError = arrow_err.into();
414        assert!(err.is_arrow_error());
415        assert!(!err.is_unsupported_type());
416        assert!(err.to_string().contains("arrow error"));
417    }
418
419    // ═══════════════════════════════════════════════════════════════════════════
420    // Display and Debug Tests
421    // ═══════════════════════════════════════════════════════════════════════════
422
423    #[test]
424    fn test_error_debug() {
425        let err = ArrowConversionError::unsupported_type(99);
426        let debug_str = format!("{err:?}");
427        assert!(debug_str.contains("ArrowConversionError"));
428        assert!(debug_str.contains("UnsupportedType"));
429    }
430
431    #[test]
432    fn test_unsupported_type_display() {
433        let err = ArrowConversionError::unsupported_type(127);
434        let display = err.to_string();
435        assert!(display.contains("unsupported HANA type"));
436        assert!(display.contains("127"));
437    }
438
439    #[test]
440    fn test_schema_mismatch_display() {
441        let err = ArrowConversionError::schema_mismatch(10, 5);
442        let display = err.to_string();
443        assert!(display.contains("schema mismatch"));
444        assert!(display.contains("expected 10 columns"));
445        assert!(display.contains("got 5"));
446    }
447
448    #[test]
449    fn test_value_conversion_display() {
450        let err = ArrowConversionError::value_conversion("my_column", "parse error");
451        let display = err.to_string();
452        assert!(display.contains("value conversion failed"));
453        assert!(display.contains("my_column"));
454        assert!(display.contains("parse error"));
455    }
456
457    #[test]
458    fn test_decimal_overflow_display() {
459        let err = ArrowConversionError::decimal_overflow(50, 20);
460        let display = err.to_string();
461        assert!(display.contains("decimal overflow"));
462        assert!(display.contains("precision 50"));
463        assert!(display.contains("scale 20"));
464    }
465
466    // ═══════════════════════════════════════════════════════════════════════════
467    // Predicate Exhaustive Tests
468    // ═══════════════════════════════════════════════════════════════════════════
469
470    #[test]
471    fn test_all_predicates_false_for_unsupported_type() {
472        let err = ArrowConversionError::unsupported_type(1);
473        assert!(err.is_unsupported_type());
474        assert!(!err.is_schema_mismatch());
475        assert!(!err.is_value_conversion());
476        assert!(!err.is_decimal_overflow());
477        assert!(!err.is_arrow_error());
478        assert!(!err.is_hdbconnect_error());
479        assert!(!err.is_lob_streaming());
480        assert!(!err.is_invalid_precision());
481        assert!(!err.is_invalid_scale());
482    }
483
484    #[test]
485    fn test_all_predicates_false_for_schema_mismatch() {
486        let err = ArrowConversionError::schema_mismatch(1, 2);
487        assert!(!err.is_unsupported_type());
488        assert!(err.is_schema_mismatch());
489        assert!(!err.is_value_conversion());
490        assert!(!err.is_decimal_overflow());
491        assert!(!err.is_arrow_error());
492        assert!(!err.is_hdbconnect_error());
493        assert!(!err.is_lob_streaming());
494        assert!(!err.is_invalid_precision());
495        assert!(!err.is_invalid_scale());
496    }
497
498    #[test]
499    fn test_all_predicates_false_for_lob_streaming() {
500        let err = ArrowConversionError::lob_streaming("test");
501        assert!(!err.is_unsupported_type());
502        assert!(!err.is_schema_mismatch());
503        assert!(!err.is_value_conversion());
504        assert!(!err.is_decimal_overflow());
505        assert!(!err.is_arrow_error());
506        assert!(!err.is_hdbconnect_error());
507        assert!(err.is_lob_streaming());
508        assert!(!err.is_invalid_precision());
509        assert!(!err.is_invalid_scale());
510    }
511
512    // ═══════════════════════════════════════════════════════════════════════════
513    // Edge Case Tests
514    // ═══════════════════════════════════════════════════════════════════════════
515
516    #[test]
517    fn test_empty_column_name() {
518        let err = ArrowConversionError::value_conversion("", "error");
519        assert!(err.is_value_conversion());
520    }
521
522    #[test]
523    fn test_empty_message() {
524        let err = ArrowConversionError::lob_streaming("");
525        assert!(err.is_lob_streaming());
526    }
527
528    #[test]
529    fn test_unicode_in_messages() {
530        let err = ArrowConversionError::value_conversion("列名", "无效数据");
531        assert!(err.is_value_conversion());
532        assert!(err.to_string().contains("列名"));
533    }
534
535    #[test]
536    fn test_zero_schema_mismatch() {
537        let err = ArrowConversionError::schema_mismatch(0, 0);
538        assert!(err.is_schema_mismatch());
539    }
540
541    #[test]
542    fn test_negative_type_id() {
543        let err = ArrowConversionError::unsupported_type(-1);
544        assert!(err.is_unsupported_type());
545    }
546
547    #[test]
548    fn test_negative_scale() {
549        let err = ArrowConversionError::decimal_overflow(10, -5);
550        assert!(err.is_decimal_overflow());
551        assert!(err.to_string().contains("scale -5"));
552    }
553
554    // ═══════════════════════════════════════════════════════════════════════════
555    // Error Classification Tests
556    // ═══════════════════════════════════════════════════════════════════════════
557
558    #[test]
559    fn test_is_recoverable() {
560        // Non-recoverable errors
561        assert!(!ArrowConversionError::unsupported_type(42).is_recoverable());
562        assert!(!ArrowConversionError::schema_mismatch(1, 2).is_recoverable());
563        assert!(!ArrowConversionError::value_conversion("col", "msg").is_recoverable());
564        assert!(!ArrowConversionError::decimal_overflow(38, 10).is_recoverable());
565        assert!(!ArrowConversionError::invalid_precision("msg").is_recoverable());
566        assert!(!ArrowConversionError::invalid_scale("msg").is_recoverable());
567
568        // Recoverable errors
569        assert!(ArrowConversionError::lob_streaming("network timeout").is_recoverable());
570    }
571
572    #[test]
573    fn test_is_configuration_error() {
574        // Configuration errors
575        assert!(ArrowConversionError::unsupported_type(42).is_configuration_error());
576        assert!(ArrowConversionError::schema_mismatch(1, 2).is_configuration_error());
577        assert!(ArrowConversionError::invalid_precision("msg").is_configuration_error());
578        assert!(ArrowConversionError::invalid_scale("msg").is_configuration_error());
579
580        // Non-configuration errors
581        assert!(!ArrowConversionError::value_conversion("col", "msg").is_configuration_error());
582        assert!(!ArrowConversionError::decimal_overflow(38, 10).is_configuration_error());
583        assert!(!ArrowConversionError::lob_streaming("msg").is_configuration_error());
584    }
585
586    #[test]
587    fn test_is_data_error() {
588        // Data errors
589        assert!(ArrowConversionError::value_conversion("col", "msg").is_data_error());
590        assert!(ArrowConversionError::decimal_overflow(38, 10).is_data_error());
591
592        // Non-data errors
593        assert!(!ArrowConversionError::unsupported_type(42).is_data_error());
594        assert!(!ArrowConversionError::schema_mismatch(1, 2).is_data_error());
595        assert!(!ArrowConversionError::lob_streaming("msg").is_data_error());
596        assert!(!ArrowConversionError::invalid_precision("msg").is_data_error());
597    }
598
599    #[test]
600    fn test_error_classification_mutual_exclusivity() {
601        // Each error should be in exactly one classification category
602        let config_err = ArrowConversionError::unsupported_type(42);
603        assert!(config_err.is_configuration_error());
604        assert!(!config_err.is_data_error());
605        assert!(!config_err.is_recoverable());
606
607        let data_err = ArrowConversionError::value_conversion("col", "msg");
608        assert!(!data_err.is_configuration_error());
609        assert!(data_err.is_data_error());
610        assert!(!data_err.is_recoverable());
611
612        let recoverable = ArrowConversionError::lob_streaming("timeout");
613        assert!(!recoverable.is_configuration_error());
614        assert!(!recoverable.is_data_error());
615        assert!(recoverable.is_recoverable());
616    }
617
618    #[test]
619    fn test_value_conversion_with_source() {
620        let source = std::io::Error::new(std::io::ErrorKind::Other, "parse failed");
621        let err = ArrowConversionError::value_conversion_with_source("col1", "failed", source);
622        assert!(err.is_value_conversion());
623        assert!(err.is_data_error());
624        assert!(err.to_string().contains("col1"));
625    }
626}