1use serde::{de, ser};
12use std::{error::Error as StdError, fmt, io, result, str::Utf8Error};
13
14pub type Result<T, E = Error> = result::Result<T, E>;
16
17type BoxedError = Box<dyn StdError + Send + Sync>;
18
19#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22#[allow(missing_docs)]
23pub enum Error {
24 #[error("invalid params: {0}")]
25 InvalidParams(#[source] BoxedError),
26 #[error("network error: {0}")]
27 Network(#[source] BoxedError),
28 #[error("compression error: {0}")]
29 Compression(#[source] BoxedError),
30 #[error("decompression error: {0}")]
31 Decompression(#[source] BoxedError),
32 #[error("no rows returned by a query that expected to return at least one row")]
33 RowNotFound,
34 #[error("sequences must have a known size ahead of time")]
35 SequenceMustHaveLength,
36 #[error("`deserialize_any` is not supported")]
37 DeserializeAnyNotSupported,
38 #[error("not enough data, probably a row type mismatches a database schema")]
39 NotEnoughData,
40 #[error("string is not valid utf8")]
41 InvalidUtf8Encoding(#[from] Utf8Error),
42 #[error("tag for enum is not valid")]
43 InvalidTagEncoding(usize),
44 #[error("max number of types in the Variant data type is 255, got {0}")]
45 VariantDiscriminatorIsOutOfBound(usize),
46 #[error("a custom error message from serde: {0}")]
47 Custom(String),
48 #[error("background worker task has exited; the inserter is no longer accepting commands")]
52 WorkerExited,
53 #[error("server error code {code}: {message}")]
59 ServerException {
60 code: i32,
64 name: Option<String>,
68 message: String,
72 stack_trace: Option<String>,
75 },
76 #[error("bad response: {0}")]
77 BadResponse(String),
78 #[error("transient transport failure: {0}")]
82 Transient(String),
83 #[error("connect failed: {0}")]
87 Connect(String),
88 #[error("timeout expired")]
89 TimedOut,
90 #[error("error while parsing columns header from the response: {0}")]
91 InvalidColumnsHeader(#[source] BoxedError),
92 #[error("schema mismatch: {0}")]
93 SchemaMismatch(String),
94 #[error("unsupported: {0}")]
95 Unsupported(String),
96 #[cfg(feature = "dynamic")]
99 #[cfg_attr(docsrs, doc(cfg(feature = "dynamic")))]
100 #[error(transparent)]
101 Dynamic(#[from] crate::dynamic::DynamicError),
102 #[error("{0}")]
103 Other(BoxedError),
104}
105
106impl From<clickhouse::error::Error> for Error {
110 fn from(error: clickhouse::error::Error) -> Self {
111 use clickhouse::error::Error as Up;
112 match error {
113 Up::InvalidParams(e) => Self::InvalidParams(e),
114 Up::Network(e) => Self::Network(e),
115 Up::Compression(e) => Self::Compression(e),
116 Up::Decompression(e) => Self::Decompression(e),
117 Up::RowNotFound => Self::RowNotFound,
118 Up::SequenceMustHaveLength => Self::SequenceMustHaveLength,
119 Up::DeserializeAnyNotSupported => Self::DeserializeAnyNotSupported,
120 Up::NotEnoughData => Self::NotEnoughData,
121 Up::InvalidUtf8Encoding(e) => Self::InvalidUtf8Encoding(e),
122 Up::InvalidTagEncoding(n) => Self::InvalidTagEncoding(n),
123 Up::VariantDiscriminatorIsOutOfBound(n) => Self::VariantDiscriminatorIsOutOfBound(n),
124 Up::Custom(s) => Self::Custom(s),
125 Up::BadResponse(s) => Self::BadResponse(s),
126 Up::TimedOut => Self::TimedOut,
127 Up::InvalidColumnsHeader(e) => Self::InvalidColumnsHeader(e),
128 Up::SchemaMismatch(s) => Self::SchemaMismatch(s),
129 Up::Unsupported(s) => Self::Unsupported(s),
130 other => Self::Other(Box::new(other)),
131 }
132 }
133}
134
135impl ser::Error for Error {
136 fn custom<T: fmt::Display>(msg: T) -> Self {
137 Self::Custom(msg.to_string())
138 }
139}
140
141impl de::Error for Error {
142 fn custom<T: fmt::Display>(msg: T) -> Self {
143 Self::Custom(msg.to_string())
144 }
145}
146
147impl From<Error> for io::Error {
148 fn from(error: Error) -> Self {
149 io::Error::other(error)
150 }
151}
152
153impl From<io::Error> for Error {
154 fn from(error: io::Error) -> Self {
155 match error.downcast::<Self>() {
158 Ok(inner) => inner,
159 Err(other) => Self::Other(other.into()),
160 }
161 }
162}
163
164impl Error {
165 #[must_use]
176 pub fn is_retriable(&self) -> bool {
177 match self {
178 Self::Network(_) | Self::TimedOut | Self::Transient(_) | Self::Connect(_) => true,
182 Self::ServerException { code, .. } => Self::is_retriable_code(*code),
183 #[cfg(feature = "dynamic")]
186 Self::Dynamic(e) => matches!(
187 e,
188 crate::dynamic::DynamicError::SchemaFetch { .. }
189 | crate::dynamic::DynamicError::SchemaMismatch { .. }
190 | crate::dynamic::DynamicError::EmptySchema { .. }
191 ),
192 _ => false,
193 }
194 }
195
196 #[must_use]
201 pub fn is_retriable_code(code: i32) -> bool {
202 matches!(
203 code,
204 159 |
206 160 |
208 202 |
210 209 |
212 210 |
214 236 |
217 241 |
220 252 |
222 279 |
224 290 |
226 319 |
231 364 |
234 384 |
236 439 |
238 473 |
240 659 |
243 904 |
245 999
247 )
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use crate::error::Error;
254 use std::io;
255
256 #[test]
257 fn roundtrip_io_error() {
258 let orig = Error::NotEnoughData;
259
260 let orig_str = orig.to_string();
262 let io = io::Error::from(orig);
263 assert_eq!(io.kind(), io::ErrorKind::Other);
264 assert_eq!(io.to_string(), orig_str);
265
266 let orig = Error::from(io);
268 assert!(matches!(orig, Error::NotEnoughData));
269 }
270
271 #[test]
272 fn error_traits() {
273 fn assert_traits<T: std::error::Error + Send + Sync>() {}
274
275 assert_traits::<Error>();
276 }
277
278 #[test]
279 fn is_retriable_classifies_known_transient_codes() {
280 let make = |code: i32| Error::ServerException {
281 code,
282 name: None,
283 message: String::new(),
284 stack_trace: None,
285 };
286
287 for &code in &[
289 159i32, 160, 202, 209, 210, 236, 241, 252, 279, 290, 319, 364, 384, 439, 473, 659, 904,
290 999,
291 ] {
292 assert!(make(code).is_retriable(), "code {code} should be retriable");
293 }
294
295 for &code in &[36i32, 60, 117, 192, 195, 469] {
297 assert!(
298 !make(code).is_retriable(),
299 "code {code} should NOT be retriable"
300 );
301 }
302 }
303
304 #[test]
305 fn is_retriable_for_transport_errors() {
306 assert!(Error::TimedOut.is_retriable());
307 assert!(Error::Transient("pool acquire timed out".into()).is_retriable());
308 assert!(Error::Connect("no address for host".into()).is_retriable());
309 let foreign: Error = io::Error::new(io::ErrorKind::ConnectionReset, "reset").into();
311 assert!(!foreign.is_retriable());
312 }
313
314 #[test]
315 fn is_retriable_code_defaults_unknown_to_not_retriable() {
316 for &code in &[0i32, 1, 12345, 99999, i32::MAX] {
319 assert!(
320 !Error::is_retriable_code(code),
321 "unknown code {code} should default to NOT retriable"
322 );
323 }
324 }
325
326 #[cfg(feature = "dynamic")]
329 #[test]
330 fn dynamic_schema_failures_are_retriable_but_bad_data_is_not() {
331 use crate::dynamic::DynamicError;
332
333 let retriable = [
334 DynamicError::EmptySchema {
335 table: "db.t".into(),
336 },
337 DynamicError::SchemaMismatch {
338 table: "db.t".into(),
339 message: "drift".into(),
340 },
341 DynamicError::SchemaFetch {
342 table: "db.t".into(),
343 source: clickhouse::error::Error::TimedOut,
344 },
345 ];
346 for e in retriable {
347 assert!(Error::from(e).is_retriable());
348 }
349
350 let terminal = [
351 DynamicError::UnsupportedType {
352 column: "c".into(),
353 type_str: "Point".into(),
354 },
355 DynamicError::EncodingError {
356 column: "c".into(),
357 message: "UInt8 value out of range".into(),
358 },
359 ];
360 for e in terminal {
361 assert!(!Error::from(e).is_retriable());
362 }
363 }
364
365 #[cfg(feature = "dynamic")]
368 #[test]
369 fn dynamic_errors_pass_through_display_and_source() {
370 use std::error::Error as _;
371
372 let err = Error::from(crate::dynamic::DynamicError::SchemaFetch {
373 table: "db.t".into(),
374 source: clickhouse::error::Error::TimedOut,
375 });
376 assert_eq!(
377 err.to_string(),
378 "failed to fetch schema for 'db.t': timeout expired"
379 );
380 let source = err.source().expect("the query failure is the source");
382 assert_eq!(source.to_string(), "timeout expired");
383 }
384}