blob: a0399a8082f91868f6a9956015de29f89536c16a [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::backtrace::{Backtrace, BacktraceStatus};
use std::fmt;
use std::fmt::{Debug, Display, Formatter};
use chrono::{DateTime, TimeZone as _, Utc};
/// Result that is a wrapper of `Result<T, iceberg::Error>`
pub type Result<T> = std::result::Result<T, Error>;
/// ErrorKind is all kinds of Error of iceberg.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
/// The operation was rejected because the system is not in a state required for the operation’s execution.
PreconditionFailed,
/// Iceberg don't know what happened here, and no actions other than
/// just returning it back. For example, iceberg returns an internal
/// service error.
Unexpected,
/// Iceberg data is invalid.
///
/// This error is returned when we try to read a table from iceberg but
/// failed to parse its metadata or data file correctly.
///
/// The table could be invalid or corrupted.
DataInvalid,
/// Iceberg namespace already exists at creation.
NamespaceAlreadyExists,
/// Iceberg table already exists at creation.
TableAlreadyExists,
/// Iceberg namespace does not exist.
NamespaceNotFound,
/// Iceberg table does not exist.
TableNotFound,
/// Iceberg feature is not supported.
///
/// This error is returned when given iceberg feature is not supported.
FeatureUnsupported,
/// Catalog commit failed due to outdated metadata
CatalogCommitConflicts,
}
impl ErrorKind {
/// Convert self into static str.
pub fn into_static(self) -> &'static str {
self.into()
}
}
impl From<ErrorKind> for &'static str {
fn from(v: ErrorKind) -> &'static str {
match v {
ErrorKind::Unexpected => "Unexpected",
ErrorKind::DataInvalid => "DataInvalid",
ErrorKind::FeatureUnsupported => "FeatureUnsupported",
ErrorKind::TableAlreadyExists => "TableAlreadyExists",
ErrorKind::TableNotFound => "TableNotFound",
ErrorKind::NamespaceAlreadyExists => "NamespaceAlreadyExists",
ErrorKind::NamespaceNotFound => "NamespaceNotFound",
ErrorKind::PreconditionFailed => "PreconditionFailed",
ErrorKind::CatalogCommitConflicts => "CatalogCommitConflicts",
}
}
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.into_static())
}
}
/// Error is the error struct returned by all iceberg functions.
///
/// ## Display
///
/// Error can be displayed in two ways:
///
/// - Via `Display`: like `err.to_string()` or `format!("{err}")`
///
/// Error will be printed in a single line:
///
/// ```shell
/// Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"
/// ```
///
/// - Via `Debug`: like `format!("{err:?}")`
///
/// Error will be printed in multi lines with more details and backtraces (if captured):
///
/// ```shell
/// Unexpected => something wrong happened
///
/// Context:
/// path: /path/to/file
/// called: send_async
///
/// Source: networking error
///
/// Backtrace:
/// 0: iceberg::error::Error::new
/// at ./src/error.rs:197:24
/// 1: iceberg::error::tests::generate_error
/// at ./src/error.rs:241:9
/// 2: iceberg::error::tests::test_error_debug_with_backtrace::{{closure}}
/// at ./src/error.rs:305:41
/// ...
/// ```
pub struct Error {
kind: ErrorKind,
message: String,
context: Vec<(&'static str, String)>,
source: Option<anyhow::Error>,
backtrace: Backtrace,
retryable: bool,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
if !self.context.is_empty() {
write!(f, ", context: {{ ")?;
write!(
f,
"{}",
self.context
.iter()
.map(|(k, v)| format!("{k}: {v}"))
.collect::<Vec<_>>()
.join(", ")
)?;
write!(f, " }}")?;
}
if !self.message.is_empty() {
write!(f, " => {}", self.message)?;
}
if let Some(source) = &self.source {
write!(f, ", source: {source}")?;
}
Ok(())
}
}
impl Debug for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// If alternate has been specified, we will print like Debug.
if f.alternate() {
let mut de = f.debug_struct("Error");
de.field("kind", &self.kind);
de.field("message", &self.message);
de.field("context", &self.context);
de.field("source", &self.source);
de.field("backtrace", &self.backtrace);
return de.finish();
}
write!(f, "{}", self.kind)?;
if !self.message.is_empty() {
write!(f, " => {}", self.message)?;
}
writeln!(f)?;
if !self.context.is_empty() {
writeln!(f)?;
writeln!(f, "Context:")?;
for (k, v) in self.context.iter() {
writeln!(f, " {k}: {v}")?;
}
}
if let Some(source) = &self.source {
writeln!(f)?;
writeln!(f, "Source: {source:#}")?;
}
if self.backtrace.status() == BacktraceStatus::Captured {
writeln!(f)?;
writeln!(f, "Backtrace:")?;
writeln!(f, "{}", self.backtrace)?;
}
Ok(())
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_ref().map(|v| v.as_ref())
}
}
impl Error {
/// Create a new Error with error kind and message.
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
context: Vec::default(),
source: None,
// `Backtrace::capture()` will check if backtrace has been enabled
// internally. It's zero cost if backtrace is disabled.
backtrace: Backtrace::capture(),
retryable: false,
}
}
/// Set retryable of the error.
pub fn with_retryable(mut self, retryable: bool) -> Self {
self.retryable = retryable;
self
}
/// Add more context in error.
pub fn with_context(mut self, key: &'static str, value: impl Into<String>) -> Self {
self.context.push((key, value.into()));
self
}
/// Set source for error.
///
/// # Notes
///
/// If the source has been set, we will raise a panic here.
pub fn with_source(mut self, src: impl Into<anyhow::Error>) -> Self {
debug_assert!(self.source.is_none(), "the source error has been set");
self.source = Some(src.into());
self
}
/// Set the backtrace for error.
///
/// This function is served as testing purpose and not intended to be called
/// by users.
#[cfg(test)]
fn with_backtrace(mut self, backtrace: Backtrace) -> Self {
self.backtrace = backtrace;
self
}
/// Return error's backtrace.
///
/// Note: the standard way of exposing backtrace is the unstable feature [`error_generic_member_access`](https://github.com/rust-lang/rust/issues/99301).
/// We don't provide it as it requires nightly rust.
///
/// If you just want to print error with backtrace, use `Debug`, like `format!("{err:?}")`.
///
/// If you use nightly rust, and want to access `iceberg::Error`'s backtrace in the standard way, you can
/// implement a new type like this:
///
/// ```ignore
/// // assume you already have `#![feature(error_generic_member_access)]` on the top of your crate
///
/// #[derive(::std::fmt::Debug)]
/// pub struct IcebergError(iceberg::Error);
///
/// impl std::fmt::Display for IcebergError {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// self.0.fmt(f)
/// }
/// }
///
/// impl std::error::Error for IcebergError {
/// fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) {
/// request.provide_ref::<std::backtrace::Backtrace>(self.0.backtrace());
/// }
///
/// fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
/// self.0.source()
/// }
/// }
/// ```
///
/// Additionally, you can add a clippy lint to prevent usage of the original `iceberg::Error` type.
/// ```toml
/// disallowed-types = [
/// { path = "iceberg::Error", reason = "Please use `my_crate::IcebergError` instead." },
/// ]
/// ```
pub fn backtrace(&self) -> &Backtrace {
&self.backtrace
}
/// Return error's kind.
///
/// Users can use this method to check error's kind and take actions.
pub fn kind(&self) -> ErrorKind {
self.kind
}
/// Return error's retryable status
pub fn retryable(&self) -> bool {
self.retryable
}
/// Return error's message.
#[inline]
pub fn message(&self) -> &str {
self.message.as_str()
}
}
macro_rules! define_from_err {
($source: path, $error_kind: path, $msg: expr) => {
impl std::convert::From<$source> for crate::error::Error {
fn from(v: $source) -> Self {
Self::new($error_kind, $msg).with_source(v)
}
}
};
}
define_from_err!(
std::str::Utf8Error,
ErrorKind::Unexpected,
"handling invalid utf-8 characters"
);
define_from_err!(
core::num::ParseIntError,
ErrorKind::Unexpected,
"parsing integer from string"
);
define_from_err!(
std::array::TryFromSliceError,
ErrorKind::DataInvalid,
"failed to convert byte slice to array"
);
define_from_err!(
std::num::TryFromIntError,
ErrorKind::DataInvalid,
"failed to convert integer"
);
define_from_err!(
chrono::ParseError,
ErrorKind::DataInvalid,
"Failed to parse string to date or time"
);
define_from_err!(
uuid::Error,
ErrorKind::DataInvalid,
"Failed to convert between uuid und iceberg value"
);
define_from_err!(
apache_avro::Error,
ErrorKind::DataInvalid,
"Failure in conversion with avro"
);
define_from_err!(
url::ParseError,
ErrorKind::DataInvalid,
"Failed to parse url"
);
define_from_err!(
reqwest::Error,
ErrorKind::Unexpected,
"Failed to execute http request"
);
define_from_err!(
serde_json::Error,
ErrorKind::DataInvalid,
"Failed to parse json string"
);
define_from_err!(
parquet::errors::ParquetError,
ErrorKind::Unexpected,
"Failed to read a Parquet file"
);
define_from_err!(
futures::channel::mpsc::SendError,
ErrorKind::Unexpected,
"Failed to send a message to a channel"
);
define_from_err!(
arrow_schema::ArrowError,
ErrorKind::Unexpected,
"Arrow Schema Error"
);
define_from_err!(std::io::Error, ErrorKind::Unexpected, "IO Operation failed");
/// Converts a timestamp in milliseconds to `DateTime<Utc>`, handling errors.
///
/// # Arguments
///
/// * `timestamp_ms` - The timestamp in milliseconds to convert.
///
/// # Returns
///
/// This function returns a `Result<DateTime<Utc>, Error>` which is `Ok` with the `DateTime<Utc>` if the conversion is successful,
/// or an `Err` with an appropriate error if the timestamp is ambiguous or invalid.
pub(crate) fn timestamp_ms_to_utc(timestamp_ms: i64) -> Result<DateTime<Utc>> {
match Utc.timestamp_millis_opt(timestamp_ms) {
chrono::LocalResult::Single(t) => Ok(t),
chrono::LocalResult::Ambiguous(_, _) => Err(Error::new(
ErrorKind::Unexpected,
"Ambiguous timestamp with two possible results",
)),
chrono::LocalResult::None => Err(Error::new(ErrorKind::DataInvalid, "Invalid timestamp")),
}
.map_err(|e| e.with_context("timestamp value", timestamp_ms.to_string()))
}
/// Helper macro to check arguments.
///
///
/// Example:
///
/// Following example check `a > 0`, otherwise returns an error.
/// ```ignore
/// use iceberg::check;
/// ensure_data_valid!(a > 0, "{} is not positive.", a);
/// ```
#[macro_export]
macro_rules! ensure_data_valid {
($cond: expr, $fmt: literal, $($arg:tt)*) => {
if !$cond {
return Err($crate::error::Error::new($crate::error::ErrorKind::DataInvalid, format!($fmt, $($arg)*)))
}
};
}
#[cfg(test)]
mod tests {
use anyhow::anyhow;
use pretty_assertions::assert_eq;
use super::*;
fn generate_error_with_backtrace_disabled() -> Error {
Error::new(
ErrorKind::Unexpected,
"something wrong happened".to_string(),
)
.with_context("path", "/path/to/file".to_string())
.with_context("called", "send_async".to_string())
.with_source(anyhow!("networking error"))
.with_backtrace(Backtrace::disabled())
}
fn generate_error_with_backtrace_enabled() -> Error {
Error::new(
ErrorKind::Unexpected,
"something wrong happened".to_string(),
)
.with_context("path", "/path/to/file".to_string())
.with_context("called", "send_async".to_string())
.with_source(anyhow!("networking error"))
.with_backtrace(Backtrace::force_capture())
}
#[test]
fn test_error_display_without_backtrace() {
let s = format!("{}", generate_error_with_backtrace_disabled());
assert_eq!(
s,
r#"Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"#
)
}
#[test]
fn test_error_display_with_backtrace() {
let s = format!("{}", generate_error_with_backtrace_enabled());
assert_eq!(
s,
r#"Unexpected, context: { path: /path/to/file, called: send_async } => something wrong happened, source: networking error"#
)
}
#[test]
fn test_error_debug_without_backtrace() {
let s = format!("{:?}", generate_error_with_backtrace_disabled());
assert_eq!(
s,
r#"Unexpected => something wrong happened
Context:
path: /path/to/file
called: send_async
Source: networking error
"#
)
}
/// Backtrace contains build information, so we just assert the header of error content.
#[test]
fn test_error_debug_with_backtrace() {
let s = format!("{:?}", generate_error_with_backtrace_enabled());
let expected = r#"Unexpected => something wrong happened
Context:
path: /path/to/file
called: send_async
Source: networking error
Backtrace:
0:"#;
assert_eq!(&s[..expected.len()], expected,);
}
}