use datafusion::error::DataFusionError;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum TimonErrorKind {
InitializationError,
ConfigurationError,
DatabaseNotFound,
DatabaseAlreadyExists,
DatabaseCreationFailed,
DatabaseDeletionFailed,
TableNotFound,
TableAlreadyExists,
TableCreationFailed,
TableDeletionFailed,
SchemaValidationFailed,
DataInsertionFailed,
DataValidationFailed,
DataSerializationFailed,
DataDeserializationFailed,
InvalidDataFormat,
ConstraintViolation,
QueryExecutionFailed,
QueryParsingFailed,
InvalidSqlQuery,
PartitionNotFound,
NoDataAvailable,
FileSystemError,
FileNotFound,
FileCreationFailed,
FileReadFailed,
FileWriteFailed,
DirectoryCreationFailed,
PermissionDenied,
CloudStorageNotInitialized,
CloudStorageConnectionFailed,
CloudStorageUploadFailed,
CloudStorageDownloadFailed,
CloudStorageListFailed,
CloudStorageAuthenticationFailed,
CloudStorageTimeout,
SyncOperationFailed,
SyncMetadataUpdateFailed,
UsernameMismatch,
LockAcquisitionFailed,
ConcurrencyError,
MetadataCorrupted,
MetadataReadFailed,
MetadataWriteFailed,
InvalidInput,
MissingRequiredField,
InvalidFieldType,
InvalidDateRange,
InternalError,
UnexpectedError,
SecurityError,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimonError {
pub kind: TimonErrorKind,
pub message: String,
pub details: Option<String>,
pub source: Option<String>,
pub context: Option<serde_json::Value>,
}
impl TimonError {
pub fn new(kind: TimonErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
details: None,
source: None,
context: None,
}
}
#[allow(dead_code)]
pub fn with_details(kind: TimonErrorKind, message: impl Into<String>, details: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
details: Some(details.into()),
source: None,
context: None,
}
}
pub fn with_source(kind: TimonErrorKind, message: impl Into<String>, source: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
details: None,
source: Some(source.into()),
context: None,
}
}
pub fn status_code(&self) -> u16 {
match self.kind {
TimonErrorKind::DatabaseNotFound | TimonErrorKind::TableNotFound | TimonErrorKind::FileNotFound | TimonErrorKind::PartitionNotFound => 404,
TimonErrorKind::DatabaseAlreadyExists | TimonErrorKind::TableAlreadyExists => 409,
TimonErrorKind::InvalidInput
| TimonErrorKind::InvalidDataFormat
| TimonErrorKind::InvalidSqlQuery
| TimonErrorKind::InvalidFieldType
| TimonErrorKind::InvalidDateRange
| TimonErrorKind::MissingRequiredField
| TimonErrorKind::SchemaValidationFailed
| TimonErrorKind::DataValidationFailed
| TimonErrorKind::ConstraintViolation
| TimonErrorKind::QueryParsingFailed => 400,
TimonErrorKind::CloudStorageAuthenticationFailed | TimonErrorKind::UsernameMismatch => 401,
TimonErrorKind::PermissionDenied | TimonErrorKind::SecurityError => 403,
TimonErrorKind::CloudStorageConnectionFailed
| TimonErrorKind::CloudStorageUploadFailed
| TimonErrorKind::CloudStorageDownloadFailed
| TimonErrorKind::CloudStorageListFailed => 502,
TimonErrorKind::CloudStorageTimeout => 504,
_ => 500,
}
}
}
impl fmt::Display for TimonError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}: {}", self.kind, self.message)?;
if let Some(details) = &self.details {
write!(f, " ({})", details)?;
}
if let Some(source) = &self.source {
write!(f, " [Source: {}]", source)?;
}
Ok(())
}
}
impl std::error::Error for TimonError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
pub type TimonResult<T> = Result<T, TimonError>;
impl From<DataFusionError> for TimonError {
fn from(err: DataFusionError) -> Self {
let err_str = err.to_string();
let kind = if err_str.contains("planning") {
TimonErrorKind::QueryParsingFailed
} else if err_str.contains("Execution") {
TimonErrorKind::QueryExecutionFailed
} else if err_str.contains("Arrow") {
TimonErrorKind::DataSerializationFailed
} else if err_str.contains("Parquet") {
TimonErrorKind::FileReadFailed
} else if err_str.contains("Io") {
TimonErrorKind::FileSystemError
} else {
TimonErrorKind::InternalError
};
TimonError::with_source(kind, "DataFusion operation failed", err_str)
}
}
impl From<std::io::Error> for TimonError {
fn from(err: std::io::Error) -> Self {
let kind = match err.kind() {
std::io::ErrorKind::NotFound => TimonErrorKind::FileNotFound,
std::io::ErrorKind::PermissionDenied => TimonErrorKind::PermissionDenied,
std::io::ErrorKind::AlreadyExists => TimonErrorKind::FileCreationFailed,
_ => TimonErrorKind::FileSystemError,
};
TimonError::with_source(kind, "File system operation failed", err.to_string())
}
}
impl From<serde_json::Error> for TimonError {
fn from(err: serde_json::Error) -> Self {
let kind = if err.is_data() {
TimonErrorKind::DataValidationFailed
} else if err.is_syntax() {
TimonErrorKind::InvalidDataFormat
} else {
TimonErrorKind::DataSerializationFailed
};
TimonError::with_source(kind, "JSON operation failed", err.to_string())
}
}
impl From<Box<dyn std::error::Error>> for TimonError {
fn from(err: Box<dyn std::error::Error>) -> Self {
TimonError::with_source(TimonErrorKind::InternalError, "Operation failed", err.to_string())
}
}
impl From<String> for TimonError {
fn from(err: String) -> Self {
TimonError::new(TimonErrorKind::InternalError, err)
}
}
impl From<&str> for TimonError {
fn from(err: &str) -> Self {
TimonError::new(TimonErrorKind::InternalError, err)
}
}
#[macro_export]
macro_rules! database_not_found {
($db_name:expr) => {
TimonError::new(TimonErrorKind::DatabaseNotFound, format!("Database '{}' does not exist", $db_name))
};
}
#[macro_export]
macro_rules! table_not_found {
($db_name:expr, $table_name:expr) => {
TimonError::new(
TimonErrorKind::TableNotFound,
format!("Table '{}.{}' does not exist", $db_name, $table_name),
)
};
}
#[macro_export]
macro_rules! invalid_input {
($msg:expr) => {
TimonError::new(TimonErrorKind::InvalidInput, $msg)
};
($msg:expr, $details:expr) => {
TimonError::with_details(TimonErrorKind::InvalidInput, $msg, $details)
};
}
#[macro_export]
macro_rules! schema_validation_failed {
($msg:expr) => {
TimonError::new(TimonErrorKind::SchemaValidationFailed, $msg)
};
($field:expr, $reason:expr) => {
TimonError::with_details(
TimonErrorKind::SchemaValidationFailed,
format!("Schema validation failed for field '{}'", $field),
$reason,
)
};
}
#[macro_export]
macro_rules! cloud_storage_error {
($kind:expr, $msg:expr) => {
TimonError::new($kind, $msg)
};
($kind:expr, $msg:expr, $details:expr) => {
TimonError::with_details($kind, $msg, $details)
};
}
impl TimonError {
pub fn cloud_storage_not_initialized() -> Self {
TimonError::new(
TimonErrorKind::CloudStorageNotInitialized,
"CloudStorageManager is not initialized. Please call init_bucket() first.",
)
}
pub fn database_manager_not_initialized() -> Self {
TimonError::new(
TimonErrorKind::InitializationError,
"DatabaseManager is not initialized. Please call init_timon() first.",
)
}
pub fn table_not_found_for_user(table_name: &str, username: Option<&str>) -> Self {
let msg = match username {
Some(user) => format!("Table '{}' does not exist for user '{}'", table_name, user),
None => format!("Table '{}' does not exist", table_name),
};
TimonError::new(TimonErrorKind::TableNotFound, msg)
}
pub fn no_data_available(table_name: &str) -> Self {
TimonError::new(
TimonErrorKind::NoDataAvailable,
format!("Table '{}' exists but contains no data (no parquet files found)", table_name),
)
}
pub fn cloud_storage_timeout(operation: &str, timeout_secs: u64) -> Self {
TimonError::new(
TimonErrorKind::CloudStorageTimeout,
format!("Cloud storage operation '{}' timed out after {} seconds", operation, timeout_secs),
)
}
}