Skip to main content

clickhouse_c/
error.rs

1//! Error types returned by clickhouse-c-rs.
2
3use core::ffi::c_int;
4
5use crate::sys;
6
7/// Category corresponding to a clickhouse-c `CHC_ERR_*` code.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ErrorKind {
10    /// Transport operation failed or read deadline expired.
11    Io,
12    /// Stream ended before required data was available.
13    Eof,
14    /// Protocol data was invalid or unsupported.
15    Protocol,
16    /// Type name was invalid or column did not match its declared type.
17    Type,
18    /// Allocator returned a null pointer.
19    Oom,
20    /// [`CancelToken`](crate::CancelToken) was set before a read.
21    Cancelled,
22    /// Server rejected handshake with an exception. See
23    /// [`Error::server_code`] and [`Error::server_name`] for details. Query
24    /// exceptions arrive as [`Event::Exception`](crate::Event::Exception).
25    Server,
26    /// API input was invalid.
27    Usage,
28    /// I/O-independent client requires more input. Blocking client does not
29    /// return this category.
30    WouldBlock,
31    /// Error code is not represented by another variant.
32    Other(c_int),
33}
34
35impl ErrorKind {
36    pub(crate) fn from_code(code: c_int) -> Self {
37        match code {
38            sys::CHC_ERR_IO => Self::Io,
39            sys::CHC_ERR_EOF => Self::Eof,
40            sys::CHC_ERR_PROTOCOL => Self::Protocol,
41            sys::CHC_ERR_TYPE => Self::Type,
42            sys::CHC_ERR_OOM => Self::Oom,
43            sys::CHC_ERR_CANCELLED => Self::Cancelled,
44            sys::CHC_ERR_SERVER => Self::Server,
45            sys::CHC_ERR_USAGE => Self::Usage,
46            sys::CHC_WOULD_BLOCK => Self::WouldBlock,
47            other => Self::Other(other),
48        }
49    }
50}
51
52/// Error returned by this crate.
53#[derive(Debug, Clone, thiserror::Error)]
54#[error("clickhouse-c: {kind:?}: {message}")]
55pub struct Error {
56    pub kind: ErrorKind,
57    /// ClickHouse error code, or zero when `kind` is not [`ErrorKind::Server`].
58    pub server_code: i32,
59    /// Error message copied from clickhouse-c.
60    pub message: String,
61    /// Server exception name, empty when `kind` is not [`ErrorKind::Server`].
62    pub server_name: String,
63}
64
65impl Error {
66    pub(crate) fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
67        Self {
68            kind,
69            server_code: 0,
70            message: message.into(),
71            server_name: String::new(),
72        }
73    }
74
75    pub(crate) fn from_raw(code: c_int, e: &sys::chc_err) -> Self {
76        Self::new(ErrorKind::from_code(code), cstr_array_to_string(&e.msg))
77    }
78}
79
80impl From<std::io::Error> for Error {
81    fn from(e: std::io::Error) -> Self {
82        Self::new(ErrorKind::Io, e.to_string())
83    }
84}
85
86/// Result type used by this crate.
87pub type Result<T> = core::result::Result<T, Error>;
88
89fn cstr_array_to_string(buf: &[core::ffi::c_char]) -> String {
90    let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
91    let bytes: &[u8] = unsafe { core::slice::from_raw_parts(buf.as_ptr().cast::<u8>(), end) };
92    String::from_utf8_lossy(bytes).into_owned()
93}
94
95#[inline]
96pub(crate) fn check(rc: c_int, err: &sys::chc_err) -> Result<()> {
97    if rc == sys::CHC_OK {
98        Ok(())
99    } else {
100        Err(Error::from_raw(rc, err))
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    fn err_with(msg: &str) -> sys::chc_err {
109        let mut e = sys::chc_err::zeroed();
110        for (slot, b) in e.msg.iter_mut().zip(msg.bytes()) {
111            *slot = b as core::ffi::c_char;
112        }
113        e
114    }
115
116    // Compressed receive can leave stale text in `err` after successful return
117    #[test]
118    fn ok_rc_ignores_stale_err() {
119        let stale = err_with("ioless buffer drained");
120        assert!(check(sys::CHC_OK, &stale).is_ok());
121    }
122
123    #[test]
124    fn error_rc_surfaces_kind() {
125        let e = err_with("bad handshake");
126        let err = check(sys::CHC_ERR_PROTOCOL, &e).unwrap_err();
127        assert_eq!(err.kind, ErrorKind::Protocol);
128        assert_eq!(err.message, "bad handshake");
129    }
130
131    #[test]
132    fn every_c_code_maps_to_its_kind() {
133        for (code, kind) in [
134            (sys::CHC_ERR_IO, ErrorKind::Io),
135            (sys::CHC_ERR_EOF, ErrorKind::Eof),
136            (sys::CHC_ERR_PROTOCOL, ErrorKind::Protocol),
137            (sys::CHC_ERR_TYPE, ErrorKind::Type),
138            (sys::CHC_ERR_OOM, ErrorKind::Oom),
139            (sys::CHC_ERR_CANCELLED, ErrorKind::Cancelled),
140            (sys::CHC_ERR_SERVER, ErrorKind::Server),
141            (sys::CHC_ERR_USAGE, ErrorKind::Usage),
142            (sys::CHC_WOULD_BLOCK, ErrorKind::WouldBlock),
143        ] {
144            assert_eq!(ErrorKind::from_code(code), kind, "code {code}");
145        }
146    }
147
148    // Codes added to C API must not collapse into a represented variant
149    #[test]
150    fn unknown_code_keeps_its_number() {
151        assert_eq!(ErrorKind::from_code(-4242), ErrorKind::Other(-4242));
152    }
153
154    #[test]
155    fn transport_errors_become_io_kind() {
156        let err: Error = std::io::Error::other("socket went away").into();
157        assert_eq!(err.kind, ErrorKind::Io);
158        assert!(err.message.contains("socket went away"), "{err}");
159        assert_eq!(err.server_code, 0);
160        assert!(err.server_name.is_empty());
161    }
162
163    // Message array need not be NUL-terminated when C fills every byte
164    #[test]
165    fn unterminated_message_reads_to_capacity() {
166        let mut e = sys::chc_err::zeroed();
167        e.msg.fill(b'x' as core::ffi::c_char);
168        let err = check(sys::CHC_ERR_IO, &e).unwrap_err();
169        assert_eq!(err.message.len(), e.msg.len());
170    }
171}