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}