Skip to main content

keelson_core/
error.rs

1use std::fmt;
2
3/// The result type used throughout keelson.
4pub type Result<T> = std::result::Result<T, Error>;
5
6/// Everything that can go wrong while building a query.
7///
8/// Building is pure string work, so the set is small: almost every failure is
9/// either "this dialect cannot express that" or "the caller wired the pieces up
10/// inconsistently". Execution errors live in the backend crates.
11///
12/// Rendering itself is infallible — [`Expression::write_sql`](crate::Expression::write_sql)
13/// returns nothing. The rare failure is recorded on the
14/// [`SqlWriter`](crate::SqlWriter) and surfaced once, by
15/// [`build`](crate::build).
16#[derive(Debug)]
17#[non_exhaustive]
18pub enum Error {
19    /// The dialect has no syntax for named argument placeholders.
20    ///
21    /// bob models this by having a separate `DialectWithNamed` interface and
22    /// type-asserting on it; we keep one trait whose default
23    /// [`write_named_arg`](crate::Dialect::write_named_arg) records this instead.
24    NoNamedArgs,
25
26    /// A raw clause's `?` placeholders and its argument list disagree.
27    ///
28    /// The message is byte-compatible with bob's `rawError` so a ported test can
29    /// compare it directly.
30    RawArgCount {
31        /// How many `?` the clause contains.
32        placeholders: usize,
33        /// How many arguments were supplied.
34        args: usize,
35        /// The offending clause, for the message.
36        clause: String,
37    },
38
39    /// A [`Value`](crate::Value) could not be read as the requested Rust type.
40    TypeMismatch {
41        /// The Rust type that was asked for.
42        expected: &'static str,
43        /// The `Value` variant that was actually present.
44        found: &'static str,
45    },
46
47    /// A query is missing a clause it cannot be rendered without.
48    Incomplete(&'static str),
49
50    /// Two clauses were both set that are alternative spellings of one grammar
51    /// production — `LIMIT` and `FETCH` — so no statement can carry both.
52    ///
53    /// Deliberately not last-write-wins: the order mods are applied must never
54    /// change what a query means (see the modifier-ordering entry in
55    /// `docs/sql-rendering.md`), so the collision is reported instead of
56    /// resolved.
57    ConflictingClauses {
58        /// One of the colliding clauses, as its keyword.
59        first: &'static str,
60        /// The other.
61        second: &'static str,
62    },
63
64    /// A dialect-specific or generated-code failure that has no shared shape.
65    Other(String),
66}
67
68impl Error {
69    /// Shorthand for [`Error::TypeMismatch`].
70    pub fn type_mismatch(expected: &'static str, found: &'static str) -> Self {
71        Error::TypeMismatch { expected, found }
72    }
73
74    /// Shorthand for [`Error::RawArgCount`].
75    pub fn raw_arg_count(placeholders: usize, args: usize, clause: impl Into<String>) -> Self {
76        Error::RawArgCount {
77            placeholders,
78            args,
79            clause: clause.into(),
80        }
81    }
82
83    /// Shorthand for [`Error::ConflictingClauses`].
84    pub fn conflicting_clauses(first: &'static str, second: &'static str) -> Self {
85        Error::ConflictingClauses { first, second }
86    }
87
88    /// Shorthand for [`Error::Other`].
89    pub fn other(msg: impl Into<String>) -> Self {
90        Error::Other(msg.into())
91    }
92}
93
94impl fmt::Display for Error {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        match self {
97            Error::NoNamedArgs => f.write_str("Dialect does not support named arguments"),
98            Error::RawArgCount {
99                placeholders,
100                args,
101                clause,
102            } => write!(
103                f,
104                "Bad Statement: has {placeholders} placeholders but {args} args: {clause}"
105            ),
106            Error::TypeMismatch { expected, found } => {
107                write!(f, "cannot read {found} as {expected}")
108            }
109            Error::Incomplete(what) => write!(f, "query is missing {what}"),
110            Error::ConflictingClauses { first, second } => write!(
111                f,
112                "{first} and {second} are both set, but they are two spellings of one clause — set only one"
113            ),
114            Error::Other(msg) => f.write_str(msg),
115        }
116    }
117}
118
119impl std::error::Error for Error {}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn named_arg_error_reads_like_bobs() {
127        assert_eq!(
128            Error::NoNamedArgs.to_string(),
129            "Dialect does not support named arguments"
130        );
131    }
132
133    #[test]
134    fn raw_arg_count_message_is_byte_compatible_with_bob() {
135        // bob: "Bad Statement: has 2 placeholders but 0 args: <clause>"
136        let e = Error::raw_arg_count(2, 0, "SELECT a, b FROM alphabet WHERE c = ? AND d <= ?");
137        assert_eq!(
138            e.to_string(),
139            "Bad Statement: has 2 placeholders but 0 args: SELECT a, b FROM alphabet WHERE c = ? AND d <= ?"
140        );
141    }
142
143    #[test]
144    fn conflicting_clauses_names_what_the_caller_set() {
145        assert_eq!(
146            Error::conflicting_clauses("LIMIT", "FETCH").to_string(),
147            "LIMIT and FETCH are both set, but they are two spellings of one clause — set only one"
148        );
149    }
150
151    #[test]
152    fn is_a_std_error() {
153        fn takes(_: &dyn std::error::Error) {}
154        takes(&Error::Incomplete("a table"));
155    }
156}