rudb_common/error.rs
1//! The error model.
2//!
3//! `spec/04-architecture.md` section 4.9 says errors are values, `Result` is everywhere, and no
4//! path reachable from user input panics. It also says every error carries a code, a message and
5//! optionally a span into the query text, that the codes are stable because clients switch on
6//! them, and that the messages match DuckDB's where a DuckDB message is what a test asserts on.
7//!
8//! This module is where all three of those obligations live.
9
10use std::fmt;
11
12/// The result type used everywhere in the workspace.
13pub type Result<T> = std::result::Result<T, Error>;
14
15/// A byte range into the query text.
16///
17/// Half open, so `start` is the first byte and `end` is one past the last, which is what slicing
18/// wants and what every editor protocol in existence expects. Byte offsets rather than character
19/// offsets because that is what the parser has and converting is the caller's problem, once, at
20/// the point where a human is going to read it.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct Span {
23 /// First byte of the span.
24 pub start: u32,
25 /// One past the last byte of the span.
26 pub end: u32,
27}
28
29impl Span {
30 /// A span over `start .. end`.
31 #[must_use]
32 pub const fn new(start: u32, end: u32) -> Self {
33 Self { start, end }
34 }
35
36 /// The number of bytes covered, which is zero for a span that points between two characters.
37 #[must_use]
38 pub const fn len(self) -> u32 {
39 self.end.saturating_sub(self.start)
40 }
41
42 /// Whether the span covers no bytes.
43 #[must_use]
44 pub const fn is_empty(self) -> bool {
45 self.len() == 0
46 }
47}
48
49/// What kind of thing went wrong.
50///
51/// These are stable and they are part of the public interface, because a client that retries on
52/// one class of failure and gives up on another has to be able to tell them apart without reading
53/// the message. Adding a variant is a compatible change and renaming one is not.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55#[non_exhaustive]
56pub enum ErrorCode {
57 /// The text is not SQL.
58 Parser,
59 /// The text is SQL and a value in it is not one the statement can take.
60 ///
61 /// DuckDB's own line between this and [`ErrorCode::Parser`] is not one anybody would draw
62 /// twice, and it is on the wire, so it is here: `SET threads=0` is a syntax error there and a
63 /// syntax error here.
64 Syntax,
65 /// The text is SQL and it does not mean anything, for example a column that is not in scope.
66 Binder,
67 /// A named object is missing, or one that should be missing is not.
68 Catalog,
69 /// A value will not convert to the type it is being asked for.
70 Conversion,
71 /// A value is outside what its type can hold.
72 OutOfRange,
73 /// An argument is wrong in a way that is not a type error, for example a negative length.
74 InvalidInput,
75 /// An allocation failed or a memory limit was reached. An error, never an abort.
76 OutOfMemory,
77 /// The filesystem, the network or the object store said no.
78 Io,
79 /// It is in the plan and it is not built yet.
80 NotImplemented,
81 /// A primary key, unique, not null or check constraint was violated.
82 Constraint,
83 /// A conflict, an abort, or a statement issued outside a transaction that needs one.
84 Transaction,
85 /// The query was cancelled. Cooperative, checked at morsel boundaries.
86 Interrupt,
87 /// An invariant this code is responsible for does not hold. Always a bug here, never in the
88 /// query.
89 Internal,
90}
91
92impl ErrorCode {
93 /// The prefix DuckDB puts on a message with this code.
94 ///
95 /// Compatibility obligation from `spec/12-duckdb-compat.md` section 12.5: a great many tests
96 /// in the wild assert on the exact text of an error, so the prefix is DuckDB's spelling
97 /// including the parts that look like typos. `Not implemented Error` really is capitalised
98 /// that way upstream, and `INTERNAL Error` really is shouted.
99 #[must_use]
100 pub const fn duckdb_name(self) -> &'static str {
101 match self {
102 Self::Parser => "Parser Error",
103 Self::Syntax => "Syntax Error",
104 Self::Binder => "Binder Error",
105 Self::Catalog => "Catalog Error",
106 Self::Conversion => "Conversion Error",
107 Self::OutOfRange => "Out of Range Error",
108 Self::InvalidInput => "Invalid Input Error",
109 Self::OutOfMemory => "Out of Memory Error",
110 Self::Io => "IO Error",
111 Self::NotImplemented => "Not implemented Error",
112 Self::Constraint => "Constraint Error",
113 Self::Transaction => "TransactionContext Error",
114 Self::Interrupt => "Interrupt Error",
115 Self::Internal => "INTERNAL Error",
116 }
117 }
118
119 /// Whether an error with this code says something about the query rather than about us.
120 ///
121 /// Used by the fuzzing harness in `spec/16-testing.md` section 16.4, which treats a rejected
122 /// query as a normal outcome and an internal error as a finding.
123 #[must_use]
124 pub const fn is_user_error(self) -> bool {
125 matches!(
126 self,
127 Self::Parser
128 | Self::Syntax
129 | Self::Binder
130 | Self::Catalog
131 | Self::Conversion
132 | Self::OutOfRange
133 | Self::InvalidInput
134 | Self::Constraint
135 | Self::Transaction
136 )
137 }
138}
139
140impl fmt::Display for ErrorCode {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 f.write_str(self.duckdb_name())
143 }
144}
145
146/// An error, carrying a code, a message and optionally where in the query it happened.
147///
148/// The payload is boxed so that `Error` is one pointer wide, which keeps `Result<T>` the same size
149/// as `T` for every `T` that has a niche. Errors are rare and results are returned from every
150/// function in the workspace, so the cost belongs on the rare path.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct Error(Box<Payload>);
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155struct Payload {
156 code: ErrorCode,
157 message: String,
158 span: Option<Span>,
159}
160
161impl Error {
162 /// An error with a code and a message and no span.
163 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
164 Self(Box::new(Payload { code, message: message.into(), span: None }))
165 }
166
167 /// The same error, with the part of the query it is about.
168 #[must_use]
169 pub fn with_span(mut self, span: Span) -> Self {
170 self.0.span = Some(span);
171 self
172 }
173
174 /// What kind of thing went wrong.
175 #[must_use]
176 pub fn code(&self) -> ErrorCode {
177 self.0.code
178 }
179
180 /// The message, without the code prefix that `Display` adds.
181 #[must_use]
182 pub fn message(&self) -> &str {
183 &self.0.message
184 }
185
186 /// Where in the query text this is about, if it is about a place.
187 #[must_use]
188 pub fn span(&self) -> Option<Span> {
189 self.0.span
190 }
191
192 /// The text is not SQL.
193 pub fn parser(message: impl Into<String>) -> Self {
194 Self::new(ErrorCode::Parser, message)
195 }
196
197 /// The text is SQL and a value in it is not one the statement can take.
198 pub fn syntax(message: impl Into<String>) -> Self {
199 Self::new(ErrorCode::Syntax, message)
200 }
201
202 /// The text is SQL and it does not mean anything.
203 pub fn binder(message: impl Into<String>) -> Self {
204 Self::new(ErrorCode::Binder, message)
205 }
206
207 /// A named object is missing, or one that should be missing is not.
208 pub fn catalog(message: impl Into<String>) -> Self {
209 Self::new(ErrorCode::Catalog, message)
210 }
211
212 /// A value will not convert to the type it is being asked for.
213 pub fn conversion(message: impl Into<String>) -> Self {
214 Self::new(ErrorCode::Conversion, message)
215 }
216
217 /// A value is outside what its type can hold.
218 pub fn out_of_range(message: impl Into<String>) -> Self {
219 Self::new(ErrorCode::OutOfRange, message)
220 }
221
222 /// An argument is wrong in a way that is not a type error.
223 pub fn invalid_input(message: impl Into<String>) -> Self {
224 Self::new(ErrorCode::InvalidInput, message)
225 }
226
227 /// An allocation failed or a memory limit was reached.
228 pub fn out_of_memory(message: impl Into<String>) -> Self {
229 Self::new(ErrorCode::OutOfMemory, message)
230 }
231
232 /// The filesystem, the network or the object store said no.
233 pub fn io(message: impl Into<String>) -> Self {
234 Self::new(ErrorCode::Io, message)
235 }
236
237 /// It is in the plan and it is not built yet.
238 pub fn not_implemented(message: impl Into<String>) -> Self {
239 Self::new(ErrorCode::NotImplemented, message)
240 }
241
242 /// A constraint was violated.
243 pub fn constraint(message: impl Into<String>) -> Self {
244 Self::new(ErrorCode::Constraint, message)
245 }
246
247 /// A conflict, an abort, or a statement issued outside a transaction that needs one.
248 pub fn transaction(message: impl Into<String>) -> Self {
249 Self::new(ErrorCode::Transaction, message)
250 }
251
252 /// The query was cancelled.
253 pub fn interrupt(message: impl Into<String>) -> Self {
254 Self::new(ErrorCode::Interrupt, message)
255 }
256
257 /// An invariant this code is responsible for does not hold.
258 ///
259 /// Reaching this is always a bug in the database and never a bug in the query, which is why it
260 /// reads differently from the others and why the fuzzer treats it as a finding.
261 pub fn internal(message: impl Into<String>) -> Self {
262 Self::new(ErrorCode::Internal, message)
263 }
264}
265
266impl fmt::Display for Error {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 write!(f, "{}: {}", self.0.code, self.0.message)
269 }
270}
271
272impl std::error::Error for Error {}
273
274impl From<std::io::Error> for Error {
275 fn from(error: std::io::Error) -> Self {
276 Self::io(error.to_string())
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::{Error, ErrorCode, Span};
283
284 #[test]
285 fn an_error_prints_the_way_duckdb_prints_it() {
286 let error = Error::binder("Referenced column \"nope\" not found in FROM clause!");
287 assert_eq!(
288 error.to_string(),
289 "Binder Error: Referenced column \"nope\" not found in FROM clause!"
290 );
291 }
292
293 #[test]
294 fn a_result_is_no_wider_than_the_value_in_it() {
295 // The reason the payload is boxed. If this ever fails, every function in the workspace
296 // got more expensive to return from and nobody noticed.
297 assert_eq!(size_of::<Error>(), size_of::<usize>());
298 assert_eq!(size_of::<Result<String, Error>>(), size_of::<String>());
299 }
300
301 #[test]
302 fn a_span_survives_being_attached() {
303 let error = Error::parser("syntax error at or near \"FROM\"").with_span(Span::new(7, 11));
304 assert_eq!(error.span(), Some(Span::new(7, 11)));
305 assert_eq!(error.span().map(Span::len), Some(4));
306 assert_eq!(error.code(), ErrorCode::Parser);
307 }
308
309 #[test]
310 fn the_fuzzer_can_tell_our_bugs_from_the_query_s_bugs() {
311 assert!(ErrorCode::Binder.is_user_error());
312 assert!(ErrorCode::Conversion.is_user_error());
313 assert!(!ErrorCode::Internal.is_user_error());
314 assert!(!ErrorCode::OutOfMemory.is_user_error());
315 // Not implemented is ours rather than the query's, because the query was legitimate and we
316 // are the reason it did not run.
317 assert!(!ErrorCode::NotImplemented.is_user_error());
318 }
319}