1use datafusion::arrow::error::ArrowError;
4use datafusion::error::DataFusionError;
5
6use crate::BackendKind;
7
8pub type Result<T, E = EngineError> = core::result::Result<T, E>;
10
11#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum EngineError {
18 #[error("I/O error: {0}")]
20 Io(#[from] std::io::Error),
21
22 #[error("allocation of {bytes} bytes (align {align}) failed: {detail}")]
24 Allocation {
25 bytes: usize,
27 align: usize,
29 detail: String,
31 },
32
33 #[error("{backend} device error: {detail}")]
35 Device {
36 backend: BackendKind,
38 detail: String,
40 },
41
42 #[error("execution error: {0}")]
44 Execution(String),
45
46 #[error("plan error: {0}")]
48 Plan(String),
49
50 #[error("format error: {0}")]
52 Format(String),
53
54 #[error("unsupported: {feature} ({detail})")]
59 Unsupported {
60 feature: &'static str,
62 detail: String,
64 },
65
66 #[error(transparent)]
68 Arrow(#[from] ArrowError),
69
70 #[error(transparent)]
72 DataFusion(#[from] DataFusionError),
73}
74
75impl EngineError {
76 pub fn unsupported(feature: &'static str, detail: impl Into<String>) -> Self {
78 Self::Unsupported {
79 feature,
80 detail: detail.into(),
81 }
82 }
83
84 pub fn execution(detail: impl Into<String>) -> Self {
86 Self::Execution(detail.into())
87 }
88
89 pub fn plan(detail: impl Into<String>) -> Self {
91 Self::Plan(detail.into())
92 }
93
94 pub fn format(detail: impl Into<String>) -> Self {
96 Self::Format(detail.into())
97 }
98
99 pub fn device(backend: BackendKind, detail: impl Into<String>) -> Self {
101 Self::Device {
102 backend,
103 detail: detail.into(),
104 }
105 }
106
107 pub fn allocation(bytes: usize, align: usize, detail: impl Into<String>) -> Self {
109 Self::Allocation {
110 bytes,
111 align,
112 detail: detail.into(),
113 }
114 }
115
116 pub fn is_unsupported(&self) -> bool {
119 matches!(self, Self::Unsupported { .. })
120 }
121}
122
123impl From<EngineError> for DataFusionError {
124 fn from(err: EngineError) -> Self {
125 match err {
126 EngineError::DataFusion(inner) => inner,
127 other => DataFusionError::External(Box::new(other)),
128 }
129 }
130}
131
132#[cfg(test)]
133#[allow(clippy::unwrap_used, clippy::expect_used)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn unsupported_is_detectable() {
139 let err = EngineError::unsupported("metal.hash_join", "not in v1");
140 assert!(err.is_unsupported());
141 assert_eq!(err.to_string(), "unsupported: metal.hash_join (not in v1)");
142 }
143
144 #[test]
145 fn datafusion_round_trip_unwraps_inner_error() {
146 let inner = DataFusionError::Plan("boom".to_owned());
147 let wrapped = EngineError::from(inner);
148 let back: DataFusionError = wrapped.into();
149 assert!(matches!(back, DataFusionError::Plan(ref m) if m == "boom"));
150 }
151
152 #[test]
153 fn other_errors_become_external() {
154 let back: DataFusionError = EngineError::format("truncated footer").into();
155 assert!(matches!(back, DataFusionError::External(_)));
156 assert!(back.to_string().contains("truncated footer"));
157 }
158
159 #[test]
160 fn io_errors_convert() {
161 let io = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
162 let err: EngineError = io.into();
163 assert!(matches!(err, EngineError::Io(_)));
164 }
165}