1#![forbid(unsafe_code)]
2use crate::SqlError;
10
11pub const SUCCESS: &str = "00000";
15pub const FEATURE_NOT_SUPPORTED: &str = "0A000";
17pub const DATA_EXCEPTION: &str = "22000";
19pub const INVALID_AUTHORIZATION: &str = "28000";
21pub const SYNTAX_ERROR: &str = "42000";
23pub const INSUFFICIENT_PRIVILEGE: &str = "42501";
25pub const UNDEFINED_TABLE: &str = "42P01";
27pub const QUERY_CANCELLED: &str = "57014";
29pub const QUERY_TIMEOUT: &str = "57P05";
31pub const SYSTEM_ERROR: &str = "58000";
33pub const INTERNAL_ERROR: &str = "XX000";
35pub const GENERAL_ERROR: &str = "HY000";
37
38pub fn sqlstate_for(error: &SqlError) -> &'static str {
45 match error {
46 SqlError::EmptyQuery => SYNTAX_ERROR,
47 SqlError::EmptyTableName => SYNTAX_ERROR,
48 SqlError::Unsupported { .. } => FEATURE_NOT_SUPPORTED,
49 SqlError::InvalidTableFunction { .. } => SYNTAX_ERROR,
50 SqlError::DataFusion { message } => datafusion_sqlstate(message),
51 SqlError::Optimizer(_) => INTERNAL_ERROR,
52 SqlError::AccessDenied { .. } => INSUFFICIENT_PRIVILEGE,
53 SqlError::OperationCancelled { .. } => QUERY_CANCELLED,
54 SqlError::Timeout { .. } => QUERY_TIMEOUT,
55 }
56}
57
58fn datafusion_sqlstate(message: &str) -> &'static str {
75 if message.starts_with("SQL error: ") {
77 return SYNTAX_ERROR;
78 }
79 if message.starts_with("Error during planning: ") {
80 return if message.contains("not found") || message.contains("No table named") {
83 UNDEFINED_TABLE
84 } else {
85 SYNTAX_ERROR
86 };
87 }
88 if message.starts_with("Schema error: ") {
91 return SYNTAX_ERROR;
92 }
93 if message.starts_with("This feature is not implemented: ") {
94 return FEATURE_NOT_SUPPORTED;
95 }
96 if message.starts_with("Arrow error: ") {
98 return DATA_EXCEPTION;
99 }
100 if message.starts_with("Parquet error: ")
102 || message.starts_with("Object Store error: ")
103 || message.starts_with("IO error: ")
104 || message.starts_with("External error: ")
105 || message.starts_with("FFI error: ")
106 || message.starts_with("Substrait error: ")
107 {
108 return SYSTEM_ERROR;
109 }
110 if message.starts_with("Resources exhausted: ")
113 || message.starts_with("Invalid or Unsupported Configuration: ")
114 || message.starts_with("Execution error: ")
115 || message.starts_with("ExecutionJoin error: ")
116 {
117 return GENERAL_ERROR;
118 }
119 INTERNAL_ERROR
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct SqlStateError {
128 pub code: &'static str,
130 pub message: String,
132}
133
134impl SqlStateError {
135 pub fn from_sql_error(error: &SqlError) -> Self {
137 Self {
138 code: sqlstate_for(error),
139 message: error.to_string(),
140 }
141 }
142}
143
144impl std::fmt::Display for SqlStateError {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(f, "SQLSTATE {} — {}", self.code, self.message)
147 }
148}
149
150impl std::error::Error for SqlStateError {}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn empty_query_maps_to_syntax_error() {
158 let e = SqlError::EmptyQuery;
159 assert_eq!(sqlstate_for(&e), SYNTAX_ERROR);
160 }
161
162 #[test]
163 fn unsupported_maps_to_feature_not_supported() {
164 let e = SqlError::Unsupported {
165 feature: "TABLESAMPLE".into(),
166 };
167 assert_eq!(sqlstate_for(&e), FEATURE_NOT_SUPPORTED);
168 }
169
170 #[test]
171 fn unrecognised_datafusion_message_keeps_internal_error() {
172 let e = SqlError::DataFusion {
173 message: "panic in executor".into(),
174 };
175 assert_eq!(sqlstate_for(&e), INTERNAL_ERROR);
176 }
177
178 #[test]
181 fn datafusion_user_errors_do_not_map_to_internal_error() {
182 let cases: &[(&str, &str)] = &[
183 ("SQL error: ParserError(\"Expected: ...\")", SYNTAX_ERROR),
184 ("Error during planning: table 'orders' not found", UNDEFINED_TABLE),
185 ("Error during planning: No table named foo", UNDEFINED_TABLE),
186 ("Error during planning: Coercion from [Utf8] to ... failed", SYNTAX_ERROR),
187 ("Schema error: No field named custkey.", SYNTAX_ERROR),
188 ("This feature is not implemented: GROUPING SETS", FEATURE_NOT_SUPPORTED),
189 ("Arrow error: Cast error: Cannot cast 'x' to Int64", DATA_EXCEPTION),
190 ("Object Store error: Generic S3 error", SYSTEM_ERROR),
191 ("Parquet error: EOF", SYSTEM_ERROR),
192 ("IO error: broken pipe", SYSTEM_ERROR),
193 ("External error: connector failed", SYSTEM_ERROR),
194 ("Resources exhausted: memory limit", GENERAL_ERROR),
195 ("Execution error: divide by zero", GENERAL_ERROR),
196 ("Internal error: this is a bug", INTERNAL_ERROR),
197 ];
198 for (message, expected) in cases {
199 let error = SqlError::DataFusion {
200 message: (*message).to_string(),
201 };
202 assert_eq!(
203 sqlstate_for(&error),
204 *expected,
205 "wrong SQLSTATE for {message:?}"
206 );
207 }
208 }
209
210 #[test]
213 fn prefixes_match_real_datafusion_errors() {
214 use datafusion::error::DataFusionError;
215
216 let planning: SqlError =
217 DataFusionError::Plan("table 'nope' not found".to_string()).into();
218 assert_eq!(sqlstate_for(&planning), UNDEFINED_TABLE);
219
220 let internal: SqlError = DataFusionError::Internal("bug".to_string()).into();
221 assert_eq!(sqlstate_for(&internal), INTERNAL_ERROR);
222
223 let exhausted: SqlError =
224 DataFusionError::ResourcesExhausted("pool".to_string()).into();
225 assert_eq!(sqlstate_for(&exhausted), GENERAL_ERROR);
226 }
227
228 #[test]
229 fn access_denied_maps_to_insufficient_privilege() {
230 let e = SqlError::AccessDenied {
231 reason: "no read permission".into(),
232 };
233 assert_eq!(sqlstate_for(&e), INSUFFICIENT_PRIVILEGE);
234 }
235
236 #[test]
237 fn cancelled_maps_to_query_cancelled() {
238 let e = SqlError::OperationCancelled { operation_id: 42 };
239 assert_eq!(sqlstate_for(&e), QUERY_CANCELLED);
240 }
241
242 #[test]
243 fn timeout_maps_to_query_timeout() {
244 let e = SqlError::Timeout { timeout_ms: 5000 };
245 assert_eq!(sqlstate_for(&e), QUERY_TIMEOUT);
246 }
247
248 #[test]
249 fn sql_state_error_display() {
250 let e = SqlError::EmptyQuery;
251 let se = SqlStateError::from_sql_error(&e);
252 let s = se.to_string();
253 assert!(s.contains(SYNTAX_ERROR));
254 assert!(s.contains("empty"));
255 }
256
257 #[test]
258 fn sql_state_error_is_std_error() {
259 let e = SqlError::EmptyQuery;
260 let se = SqlStateError::from_sql_error(&e);
261 let _: &dyn std::error::Error = &se;
262 }
263
264 #[test]
265 fn all_sqlstate_codes_are_5_chars() {
266 for code in &[
267 SUCCESS,
268 FEATURE_NOT_SUPPORTED,
269 DATA_EXCEPTION,
270 INVALID_AUTHORIZATION,
271 SYNTAX_ERROR,
272 INSUFFICIENT_PRIVILEGE,
273 UNDEFINED_TABLE,
274 QUERY_CANCELLED,
275 QUERY_TIMEOUT,
276 SYSTEM_ERROR,
277 INTERNAL_ERROR,
278 GENERAL_ERROR,
279 ] {
280 assert_eq!(code.len(), 5, "SQLSTATE {code} must be 5 characters");
281 }
282 }
283}