Skip to main content

keelson_exec/
error.rs

1use std::fmt;
2
3use crate::executor::Family;
4
5/// Everything that can go wrong while executing a statement.
6///
7/// Build failures wrap [`keelson_core::Error`]; decode failures additionally
8/// carry the column they happened in, because "which column" is the question a
9/// failing read always raises first. Driver failures are boxed rather than
10/// enumerated: their shapes belong to the backend crates.
11#[derive(Debug)]
12#[non_exhaustive]
13pub enum ExecError {
14    /// The query failed to build. Nothing was sent to the database.
15    Build(keelson_core::Error),
16
17    /// A column's value could not be read as the requested Rust type.
18    ///
19    /// `column` is the column name, or `#N` for positional access.
20    Decode {
21        /// The column being read.
22        column: String,
23        /// Why the value would not convert — usually
24        /// [`TypeMismatch`](keelson_core::Error::TypeMismatch).
25        source: keelson_core::Error,
26    },
27
28    /// A column name was asked of a result set that has no such column.
29    MissingColumn {
30        /// The name that was asked for.
31        column: String,
32        /// The columns that are actually present, because a typo'd name is
33        /// the bug nine times out of ten.
34        available: Vec<String>,
35    },
36
37    /// `fetch_one` got zero rows.
38    RowNotFound,
39
40    /// `fetch_one` or `fetch_optional` got more than one row.
41    ///
42    /// sqlx silently takes the first; here "one" means one, so extras are an
43    /// error rather than a hidden data bug.
44    TooManyRows,
45
46    /// A [`Value`](keelson_core::Value) this backend has no binding for —
47    /// an unknown [`CustomValue`](keelson_core::CustomValue), or a variant the
48    /// engine cannot represent (e.g. `u64::MAX` where only signed 64-bit
49    /// parameters exist). Refused loudly at bind time; never stringified and
50    /// hoped for.
51    UnsupportedValue {
52        /// The value's [`type_name`](keelson_core::Value::type_name).
53        type_name: &'static str,
54        /// The backend that refused it.
55        family: Family,
56    },
57
58    /// The driver reported a failure — connection, protocol, server error.
59    Driver(Box<dyn std::error::Error + Send + Sync>),
60
61    /// A failure with no shared shape.
62    Other(String),
63}
64
65impl ExecError {
66    /// Wrap a driver error. Backends call this; applications match on it.
67    pub fn driver(e: impl std::error::Error + Send + Sync + 'static) -> Self {
68        ExecError::Driver(Box::new(e))
69    }
70
71    /// Shorthand for [`ExecError::Other`].
72    pub fn other(msg: impl Into<String>) -> Self {
73        ExecError::Other(msg.into())
74    }
75}
76
77impl From<keelson_core::Error> for ExecError {
78    fn from(e: keelson_core::Error) -> Self {
79        ExecError::Build(e)
80    }
81}
82
83impl fmt::Display for ExecError {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            ExecError::Build(e) => write!(f, "query failed to build: {e}"),
87            ExecError::Decode { column, source } => write!(f, "column \"{column}\": {source}"),
88            ExecError::MissingColumn { column, available } => write!(
89                f,
90                "no column \"{column}\" in result set (columns: {})",
91                available.join(", ")
92            ),
93            ExecError::RowNotFound => {
94                f.write_str("no rows returned where exactly one was expected")
95            }
96            ExecError::TooManyRows => {
97                f.write_str("more than one row returned where at most one was expected")
98            }
99            ExecError::UnsupportedValue { type_name, family } => {
100                write!(f, "cannot bind a {type_name} value on {family}")
101            }
102            ExecError::Driver(e) => write!(f, "driver error: {e}"),
103            ExecError::Other(msg) => f.write_str(msg),
104        }
105    }
106}
107
108impl std::error::Error for ExecError {
109    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
110        match self {
111            ExecError::Build(e) | ExecError::Decode { source: e, .. } => Some(e),
112            ExecError::Driver(e) => Some(e.as_ref()),
113            _ => None,
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn decode_errors_name_the_column() {
124        let e = ExecError::Decode {
125            column: "email".into(),
126            source: keelson_core::Error::type_mismatch("String", "NULL"),
127        };
128        assert_eq!(
129            e.to_string(),
130            "column \"email\": cannot read NULL as String"
131        );
132    }
133
134    #[test]
135    fn missing_column_lists_what_was_there() {
136        let e = ExecError::MissingColumn {
137            column: "emial".into(),
138            available: vec!["id".into(), "name".into(), "email".into()],
139        };
140        assert_eq!(
141            e.to_string(),
142            "no column \"emial\" in result set (columns: id, name, email)"
143        );
144    }
145
146    #[test]
147    fn is_a_std_error_with_a_source() {
148        let e = ExecError::Build(keelson_core::Error::Incomplete("a table"));
149        assert!(std::error::Error::source(&e).is_some());
150    }
151}