iceberg/
error.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::backtrace::{Backtrace, BacktraceStatus};
19use std::fmt;
20use std::fmt::{Debug, Display, Formatter};
21use std::sync::PoisonError;
22
23use chrono::{DateTime, TimeZone as _, Utc};
24
25/// Result that is a wrapper of `Result<T, iceberg::Error>`
26pub type Result<T> = std::result::Result<T, Error>;
27
28/// ErrorKind is all kinds of Error of iceberg.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum ErrorKind {
32    /// The operation was rejected because the system is not in a state required for the operation’s execution.
33    PreconditionFailed,
34
35    /// Iceberg don't know what happened here, and no actions other than
36    /// just returning it back. For example, iceberg returns an internal
37    /// service error.
38    Unexpected,
39
40    /// Iceberg data is invalid.
41    ///
42    /// This error is returned when we try to read a table from iceberg but
43    /// failed to parse its metadata or data file correctly.
44    ///
45    /// The table could be invalid or corrupted.
46    DataInvalid,
47
48    /// Iceberg namespace already exists at creation.
49    NamespaceAlreadyExists,
50
51    /// Iceberg table already exists at creation.
52    TableAlreadyExists,
53
54    /// Iceberg namespace does not exist.
55    NamespaceNotFound,
56
57    /// Iceberg table does not exist.
58    TableNotFound,
59
60    /// Iceberg feature is not supported.
61    ///
62    /// This error is returned when given iceberg feature is not supported.
63    FeatureUnsupported,
64
65    /// Catalog commit failed due to outdated metadata
66    CatalogCommitConflicts,
67}
68
69impl ErrorKind {
70    /// Convert self into static str.
71    pub fn into_static(self) -> &'static str {
72        self.into()
73    }
74}
75
76impl From<ErrorKind> for &'static str {
77    fn from(v: ErrorKind) -> &'static str {
78        match v {
79            ErrorKind::Unexpected => "Unexpected",
80            ErrorKind::DataInvalid => "DataInvalid",
81            ErrorKind::FeatureUnsupported => "FeatureUnsupported",
82            ErrorKind::TableAlreadyExists => "TableAlreadyExists",
83            ErrorKind::TableNotFound => "TableNotFound",
84            ErrorKind::NamespaceAlreadyExists => "NamespaceAlreadyExists",
85            ErrorKind::NamespaceNotFound => "NamespaceNotFound",
86            ErrorKind::PreconditionFailed => "PreconditionFailed",
87            ErrorKind::CatalogCommitConflicts => "CatalogCommitConflicts",
88        }
89    }
90}
91
92impl Display for ErrorKind {
93    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
94        write!(f, "{}", self.into_static())
95    }
96}
97
98/// Error is the error struct returned by all iceberg functions.
99///
100/// ## Display
101///
102/// Error can be displayed in two ways:
103///
104/// - Via `Display`: like `err.to_string()` or `format!("{err}")`
105///
106/// Error will be printed in a single line:
107///
108/// ```shell
109/// Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"
110/// ```
111///
112/// - Via `Debug`: like `format!("{err:?}")`
113///
114/// Error will be printed in multi lines with more details and backtraces (if captured):
115///
116/// ```shell
117/// Unexpected => something wrong happened
118///
119/// Context:
120///    path: /path/to/file
121///    called: send_async
122///
123/// Source: networking error
124///
125/// Backtrace:
126///    0: iceberg::error::Error::new
127///              at ./src/error.rs:197:24
128///    1: iceberg::error::tests::generate_error
129///              at ./src/error.rs:241:9
130///    2: iceberg::error::tests::test_error_debug_with_backtrace::{{closure}}
131///              at ./src/error.rs:305:41
132///    ...
133/// ```
134pub struct Error {
135    kind: ErrorKind,
136    message: String,
137
138    context: Vec<(&'static str, String)>,
139
140    source: Option<anyhow::Error>,
141    backtrace: Backtrace,
142
143    retryable: bool,
144}
145
146impl Display for Error {
147    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
148        write!(f, "{}", self.kind)?;
149
150        if !self.context.is_empty() {
151            write!(f, ", context: {{ ")?;
152            write!(
153                f,
154                "{}",
155                self.context
156                    .iter()
157                    .map(|(k, v)| format!("{k}: {v}"))
158                    .collect::<Vec<_>>()
159                    .join(", ")
160            )?;
161            write!(f, " }}")?;
162        }
163
164        if !self.message.is_empty() {
165            write!(f, " => {}", self.message)?;
166        }
167
168        if let Some(source) = &self.source {
169            write!(f, ", source: {source}")?;
170        }
171
172        Ok(())
173    }
174}
175
176impl Debug for Error {
177    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
178        // If alternate has been specified, we will print like Debug.
179        if f.alternate() {
180            let mut de = f.debug_struct("Error");
181            de.field("kind", &self.kind);
182            de.field("message", &self.message);
183            de.field("context", &self.context);
184            de.field("source", &self.source);
185            de.field("backtrace", &self.backtrace);
186            return de.finish();
187        }
188
189        write!(f, "{}", self.kind)?;
190        if !self.message.is_empty() {
191            write!(f, " => {}", self.message)?;
192        }
193        writeln!(f)?;
194
195        if !self.context.is_empty() {
196            writeln!(f)?;
197            writeln!(f, "Context:")?;
198            for (k, v) in self.context.iter() {
199                writeln!(f, "   {k}: {v}")?;
200            }
201        }
202        if let Some(source) = &self.source {
203            writeln!(f)?;
204            writeln!(f, "Source: {source:#}")?;
205        }
206
207        if self.backtrace.status() == BacktraceStatus::Captured {
208            writeln!(f)?;
209            writeln!(f, "Backtrace:")?;
210            writeln!(f, "{}", self.backtrace)?;
211        }
212
213        Ok(())
214    }
215}
216
217impl std::error::Error for Error {
218    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
219        self.source.as_ref().map(|v| v.as_ref())
220    }
221}
222
223impl Error {
224    /// Create a new Error with error kind and message.
225    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
226        Self {
227            kind,
228            message: message.into(),
229            context: Vec::default(),
230
231            source: None,
232            // `Backtrace::capture()` will check if backtrace has been enabled
233            // internally. It's zero cost if backtrace is disabled.
234            backtrace: Backtrace::capture(),
235
236            retryable: false,
237        }
238    }
239
240    /// Set retryable of the error.
241    pub fn with_retryable(mut self, retryable: bool) -> Self {
242        self.retryable = retryable;
243        self
244    }
245
246    /// Add more context in error.
247    pub fn with_context(mut self, key: &'static str, value: impl Into<String>) -> Self {
248        self.context.push((key, value.into()));
249        self
250    }
251
252    /// Set source for error.
253    ///
254    /// # Notes
255    ///
256    /// If the source has been set, we will raise a panic here.
257    pub fn with_source(mut self, src: impl Into<anyhow::Error>) -> Self {
258        debug_assert!(self.source.is_none(), "the source error has been set");
259
260        self.source = Some(src.into());
261        self
262    }
263
264    /// Set the backtrace for error.
265    ///
266    /// This function is served as testing purpose and not intended to be called
267    /// by users.
268    #[cfg(test)]
269    fn with_backtrace(mut self, backtrace: Backtrace) -> Self {
270        self.backtrace = backtrace;
271        self
272    }
273
274    /// Return error's backtrace.
275    ///
276    /// Note: the standard way of exposing backtrace is the unstable feature [`error_generic_member_access`](https://github.com/rust-lang/rust/issues/99301).
277    /// We don't provide it as it requires nightly rust.
278    ///
279    /// If you just want to print error with backtrace, use `Debug`, like `format!("{err:?}")`.
280    ///
281    /// If you use nightly rust, and want to access `iceberg::Error`'s backtrace in the standard way, you can
282    /// implement a new type like this:
283    ///
284    /// ```ignore
285    /// // assume you already have `#![feature(error_generic_member_access)]` on the top of your crate
286    ///
287    /// #[derive(::std::fmt::Debug)]
288    /// pub struct IcebergError(iceberg::Error);
289    ///
290    /// impl std::fmt::Display for IcebergError {
291    ///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292    ///         self.0.fmt(f)
293    ///     }
294    /// }
295    ///
296    /// impl std::error::Error for IcebergError {
297    ///     fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
298    ///         request.provide_ref::<std::backtrace::Backtrace>(self.0.backtrace());
299    ///     }
300    ///
301    ///     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
302    ///         self.0.source()
303    ///     }
304    /// }
305    /// ```
306    ///
307    /// Additionally, you can add a clippy lint to prevent usage of the original `iceberg::Error` type.
308    /// ```toml
309    /// disallowed-types = [
310    ///     { path = "iceberg::Error", reason = "Please use `my_crate::IcebergError` instead." },
311    /// ]
312    /// ```
313    pub fn backtrace(&self) -> &Backtrace {
314        &self.backtrace
315    }
316
317    /// Return error's kind.
318    ///
319    /// Users can use this method to check error's kind and take actions.
320    pub fn kind(&self) -> ErrorKind {
321        self.kind
322    }
323
324    /// Return error's retryable status
325    pub fn retryable(&self) -> bool {
326        self.retryable
327    }
328
329    /// Return error's message.
330    #[inline]
331    pub fn message(&self) -> &str {
332        self.message.as_str()
333    }
334}
335
336macro_rules! define_from_err {
337    ($source: path, $error_kind: path, $msg: expr) => {
338        impl std::convert::From<$source> for crate::error::Error {
339            fn from(v: $source) -> Self {
340                Self::new($error_kind, $msg).with_source(v)
341            }
342        }
343    };
344}
345
346define_from_err!(
347    std::str::Utf8Error,
348    ErrorKind::Unexpected,
349    "handling invalid utf-8 characters"
350);
351
352define_from_err!(
353    core::num::ParseIntError,
354    ErrorKind::Unexpected,
355    "parsing integer from string"
356);
357
358define_from_err!(
359    std::array::TryFromSliceError,
360    ErrorKind::DataInvalid,
361    "failed to convert byte slice to array"
362);
363
364define_from_err!(
365    std::num::TryFromIntError,
366    ErrorKind::DataInvalid,
367    "failed to convert integer"
368);
369
370define_from_err!(
371    chrono::ParseError,
372    ErrorKind::DataInvalid,
373    "Failed to parse string to date or time"
374);
375
376define_from_err!(
377    uuid::Error,
378    ErrorKind::DataInvalid,
379    "Failed to convert between uuid und iceberg value"
380);
381
382define_from_err!(
383    apache_avro::Error,
384    ErrorKind::DataInvalid,
385    "Failure in conversion with avro"
386);
387
388define_from_err!(
389    url::ParseError,
390    ErrorKind::DataInvalid,
391    "Failed to parse url"
392);
393
394define_from_err!(
395    reqwest::Error,
396    ErrorKind::Unexpected,
397    "Failed to execute http request"
398);
399
400define_from_err!(
401    serde_json::Error,
402    ErrorKind::DataInvalid,
403    "Failed to parse json string"
404);
405
406define_from_err!(
407    parquet::errors::ParquetError,
408    ErrorKind::Unexpected,
409    "Failed to read a Parquet file"
410);
411
412define_from_err!(
413    futures::channel::mpsc::SendError,
414    ErrorKind::Unexpected,
415    "Failed to send a message to a channel"
416);
417
418define_from_err!(
419    arrow_schema::ArrowError,
420    ErrorKind::Unexpected,
421    "Arrow Schema Error"
422);
423
424define_from_err!(std::io::Error, ErrorKind::Unexpected, "IO Operation failed");
425
426/// Converts a [`PoisonError`] from a poisoned lock into an [`Error`].
427pub(crate) fn lock_error<T>(e: PoisonError<T>) -> Error {
428    Error::new(ErrorKind::Unexpected, format!("Lock poisoned: {e}"))
429}
430
431/// Converts a timestamp in milliseconds to `DateTime<Utc>`, handling errors.
432///
433/// # Arguments
434///
435/// * `timestamp_ms` - The timestamp in milliseconds to convert.
436///
437/// # Returns
438///
439/// This function returns a `Result<DateTime<Utc>, Error>` which is `Ok` with the `DateTime<Utc>` if the conversion is successful,
440/// or an `Err` with an appropriate error if the timestamp is ambiguous or invalid.
441pub(crate) fn timestamp_ms_to_utc(timestamp_ms: i64) -> Result<DateTime<Utc>> {
442    match Utc.timestamp_millis_opt(timestamp_ms) {
443        chrono::LocalResult::Single(t) => Ok(t),
444        chrono::LocalResult::Ambiguous(_, _) => Err(Error::new(
445            ErrorKind::Unexpected,
446            "Ambiguous timestamp with two possible results",
447        )),
448        chrono::LocalResult::None => Err(Error::new(ErrorKind::DataInvalid, "Invalid timestamp")),
449    }
450    .map_err(|e| e.with_context("timestamp value", timestamp_ms.to_string()))
451}
452
453/// Helper macro to check arguments.
454///
455///
456/// Example:
457///
458/// Following example check `a > 0`, otherwise returns an error.
459/// ```ignore
460/// use iceberg::check;
461/// ensure_data_valid!(a > 0, "{} is not positive.", a);
462/// ```
463#[macro_export]
464macro_rules! ensure_data_valid {
465    ($cond: expr, $fmt: literal, $($arg:tt)*) => {
466        if !$cond {
467            return Err($crate::error::Error::new($crate::error::ErrorKind::DataInvalid, format!($fmt, $($arg)*)))
468        }
469    };
470}
471
472#[cfg(test)]
473mod tests {
474    use anyhow::anyhow;
475    use pretty_assertions::assert_eq;
476
477    use super::*;
478
479    fn generate_error_with_backtrace_disabled() -> Error {
480        Error::new(
481            ErrorKind::Unexpected,
482            "something wrong happened".to_string(),
483        )
484        .with_context("path", "/path/to/file".to_string())
485        .with_context("called", "send_async".to_string())
486        .with_source(anyhow!("networking error"))
487        .with_backtrace(Backtrace::disabled())
488    }
489
490    fn generate_error_with_backtrace_enabled() -> Error {
491        Error::new(
492            ErrorKind::Unexpected,
493            "something wrong happened".to_string(),
494        )
495        .with_context("path", "/path/to/file".to_string())
496        .with_context("called", "send_async".to_string())
497        .with_source(anyhow!("networking error"))
498        .with_backtrace(Backtrace::force_capture())
499    }
500
501    #[test]
502    fn test_error_display_without_backtrace() {
503        let s = format!("{}", generate_error_with_backtrace_disabled());
504        assert_eq!(
505            s,
506            r#"Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"#
507        )
508    }
509
510    #[test]
511    fn test_error_display_with_backtrace() {
512        let s = format!("{}", generate_error_with_backtrace_enabled());
513        assert_eq!(
514            s,
515            r#"Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"#
516        )
517    }
518
519    #[test]
520    fn test_error_debug_without_backtrace() {
521        let s = format!("{:?}", generate_error_with_backtrace_disabled());
522        assert_eq!(
523            s,
524            r#"Unexpected => something wrong happened
525
526Context:
527   path: /path/to/file
528   called: send_async
529
530Source: networking error
531"#
532        )
533    }
534
535    /// Backtrace contains build information, so we just assert the header of error content.
536    #[test]
537    fn test_error_debug_with_backtrace() {
538        let s = format!("{:?}", generate_error_with_backtrace_enabled());
539
540        let expected = r#"Unexpected => something wrong happened
541
542Context:
543   path: /path/to/file
544   called: send_async
545
546Source: networking error
547
548Backtrace:
549   0:"#;
550        assert_eq!(&s[..expected.len()], expected,);
551    }
552}