blob: 9733521b2509bbb062eaa1d83a30c946c0204256 [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 datafusion::error::DataFusionError;
use iceberg::{Error, ErrorKind};
/// Converts a datafusion error into an iceberg error.
pub fn from_datafusion_error(error: DataFusionError) -> Error {
let fallback_message = error.to_string();
let DataFusionError::Context(context, error) = error else {
return unexpected_datafusion_error(fallback_message);
};
let Some(kind) = parse_iceberg_error_kind(&context) else {
return unexpected_datafusion_error(fallback_message);
};
let DataFusionError::Execution(message) = error.as_ref() else {
return unexpected_datafusion_error(fallback_message);
};
Error::new(kind, strip_error_kind(kind, message))
}
/// Converts an iceberg error into a datafusion error.
pub fn to_datafusion_error(error: Error) -> DataFusionError {
DataFusionError::Context(
format!("IcebergError({})", error.kind()),
Box::new(DataFusionError::Execution(error.to_string())),
)
}
fn unexpected_datafusion_error(message: String) -> Error {
Error::new(
ErrorKind::Unexpected,
format!("DataFusion execution failed: {message}"),
)
}
fn parse_iceberg_error_kind(context: &str) -> Option<ErrorKind> {
let kind = context.strip_prefix("IcebergError(")?.strip_suffix(')')?;
match kind {
"PreconditionFailed" => Some(ErrorKind::PreconditionFailed),
"Unexpected" => Some(ErrorKind::Unexpected),
"DataInvalid" => Some(ErrorKind::DataInvalid),
"NamespaceAlreadyExists" => Some(ErrorKind::NamespaceAlreadyExists),
"TableAlreadyExists" => Some(ErrorKind::TableAlreadyExists),
"NamespaceNotFound" => Some(ErrorKind::NamespaceNotFound),
"TableNotFound" => Some(ErrorKind::TableNotFound),
"FeatureUnsupported" => Some(ErrorKind::FeatureUnsupported),
"CatalogCommitConflicts" => Some(ErrorKind::CatalogCommitConflicts),
_ => None,
}
}
fn strip_error_kind(kind: ErrorKind, message: &str) -> String {
let kind = kind.into_static();
if message == kind {
String::new()
} else {
message
.strip_prefix(&format!("{kind} => "))
.unwrap_or(message)
.to_string()
}
}
#[cfg(test)]
mod tests {
use datafusion::error::DataFusionError;
use iceberg::{Error, ErrorKind};
use super::{from_datafusion_error, to_datafusion_error};
#[test]
fn roundtrips_iceberg_error_kind_and_message() {
let error = Error::new(ErrorKind::DataInvalid, "invalid manifest");
let roundtripped = from_datafusion_error(to_datafusion_error(error));
assert_eq!(roundtripped.kind(), ErrorKind::DataInvalid);
assert_eq!(roundtripped.to_string(), "DataInvalid => invalid manifest");
}
#[test]
fn encodes_iceberg_errors_with_native_datafusion_variants() {
let error =
to_datafusion_error(Error::new(ErrorKind::DataInvalid, "invalid manifest"));
assert!(matches!(
error,
DataFusionError::Context(context, inner)
if context == "IcebergError(DataInvalid)"
&& matches!(inner.as_ref(), DataFusionError::Execution(message) if message == "DataInvalid => invalid manifest")
));
}
#[test]
fn maps_non_iceberg_datafusion_errors_to_unexpected() {
let error = from_datafusion_error(DataFusionError::Execution(
"worker failed".to_string(),
));
assert_eq!(error.kind(), ErrorKind::Unexpected);
assert_eq!(
error.to_string(),
"Unexpected => DataFusion execution failed: Execution error: worker failed"
);
}
}