Skip to main content

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    /// An entry cannot go because others depend on it, for example a schema that still holds
84    /// tables.
85    Dependency,
86    /// A sequence was asked for a value it cannot give, for example one past its maximum.
87    Sequence,
88    /// A conflict, an abort, or a statement issued outside a transaction that needs one.
89    Transaction,
90    /// A setting cannot be applied in the current engine configuration.
91    Settings,
92    /// The query was cancelled. Cooperative, checked at morsel boundaries.
93    Interrupt,
94    /// A value is one the function refuses outright rather than one of the wrong type, for example
95    /// an empty list handed to `list_reduce` with nothing to start from.
96    ParameterNotAllowed,
97    /// Two types were asked to meet and cannot, for example two structs of different sizes.
98    MismatchType,
99    /// A type cannot be used where it was put, for example a list as the key of an index.
100    InvalidType,
101    /// Something is already held by somebody else, such as a file another database is attached to.
102    ResourceInUse,
103    /// An invariant this code is responsible for does not hold. Always a bug here, never in the
104    /// query.
105    Internal,
106}
107
108impl ErrorCode {
109    /// The prefix DuckDB puts on a message with this code.
110    ///
111    /// Compatibility obligation from `spec/12-duckdb-compat.md` section 12.5: a great many tests
112    /// in the wild assert on the exact text of an error, so the prefix is DuckDB's spelling
113    /// including the parts that look like typos. `Not implemented Error` really is capitalised
114    /// that way upstream, and `INTERNAL Error` really is shouted.
115    #[must_use]
116    pub const fn duckdb_name(self) -> &'static str {
117        match self {
118            Self::Parser => "Parser Error",
119            Self::Syntax => "Syntax Error",
120            Self::Binder => "Binder Error",
121            Self::Catalog => "Catalog Error",
122            Self::Conversion => "Conversion Error",
123            Self::OutOfRange => "Out of Range Error",
124            Self::InvalidInput => "Invalid Input Error",
125            Self::OutOfMemory => "Out of Memory Error",
126            Self::Io => "IO Error",
127            Self::NotImplemented => "Not implemented Error",
128            Self::Constraint => "Constraint Error",
129            Self::Dependency => "Dependency Error",
130            Self::Sequence => "Sequence Error",
131            Self::Transaction => "TransactionContext Error",
132            Self::Settings => "Settings Error",
133            Self::Interrupt => "Interrupt Error",
134            Self::ParameterNotAllowed => "Parameter Not Allowed Error",
135            Self::MismatchType => "Mismatch Type Error",
136            Self::InvalidType => "Invalid type Error",
137            Self::ResourceInUse => "Resource In Use Error",
138            Self::Internal => "INTERNAL Error",
139        }
140    }
141
142    /// Whether an error with this code says something about the query rather than about us.
143    ///
144    /// Used by the fuzzing harness in `spec/16-testing.md` section 16.4, which treats a rejected
145    /// query as a normal outcome and an internal error as a finding.
146    #[must_use]
147    pub const fn is_user_error(self) -> bool {
148        matches!(
149            self,
150            Self::Parser
151                | Self::Syntax
152                | Self::Binder
153                | Self::Catalog
154                | Self::Conversion
155                | Self::OutOfRange
156                | Self::InvalidInput
157                | Self::Constraint
158                | Self::Dependency
159                | Self::Sequence
160                | Self::Transaction
161                | Self::Settings
162                | Self::ParameterNotAllowed
163                | Self::MismatchType
164                | Self::InvalidType
165                | Self::ResourceInUse
166        )
167    }
168}
169
170impl fmt::Display for ErrorCode {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        f.write_str(self.duckdb_name())
173    }
174}
175
176/// An error, carrying a code, a message and optionally where in the query it happened.
177///
178/// The payload is boxed so that `Error` is one pointer wide, which keeps `Result<T>` the same size
179/// as `T` for every `T` that has a niche. Errors are rare and results are returned from every
180/// function in the workspace, so the cost belongs on the rare path.
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct Error(Box<Payload>);
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185struct Payload {
186    code: ErrorCode,
187    message: String,
188    span: Option<Span>,
189    message_only: bool,
190}
191
192impl Error {
193    /// An error with a code and a message and no span.
194    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
195        Self(Box::new(Payload { code, message: message.into(), span: None, message_only: false }))
196    }
197
198    /// The same error, with the part of the query it is about.
199    #[must_use]
200    pub fn with_span(mut self, span: Span) -> Self {
201        self.0.span = Some(span);
202        self
203    }
204
205    /// Attaches the range only when a more specific caller has not already attached one.
206    #[must_use]
207    pub fn with_fallback_span(mut self, span: Span) -> Self {
208        if self.0.span.is_none() && !span.is_empty() {
209            self.0.span = Some(span);
210        }
211        self
212    }
213
214    /// What kind of thing went wrong.
215    #[must_use]
216    pub fn code(&self) -> ErrorCode {
217        self.0.code
218    }
219
220    /// The message, without the code prefix that `Display` adds.
221    #[must_use]
222    pub fn message(&self) -> &str {
223        &self.0.message
224    }
225
226    /// Where in the query text this is about, if it is about a place.
227    #[must_use]
228    pub fn span(&self) -> Option<Span> {
229        self.0.span
230    }
231
232    /// Renders this error as the structured JSON form DuckDB returns for `errors_as_json`.
233    #[must_use]
234    pub fn into_json(mut self) -> Self {
235        let exception_type = self.0.code.json_name();
236        let subtype = self.0.code.json_subtype(&self.0.message);
237        let mut fields = vec![
238            ("exception_type", exception_type.to_string()),
239            ("exception_message", self.0.message.clone()),
240        ];
241        if let Some(span) = self.0.span {
242            fields.push(("location", format!("[{},{}]", span.start, span.len())));
243            fields.push(("position", span.start.to_string()));
244        }
245        if let Some(subtype) = subtype {
246            fields.push(("error_subtype", subtype.to_string()));
247        }
248        self.0.message = json_object(&fields);
249        self.0.message_only = true;
250        self
251    }
252
253    /// The text is not SQL.
254    pub fn parser(message: impl Into<String>) -> Self {
255        Self::new(ErrorCode::Parser, message)
256    }
257
258    /// The text is SQL and a value in it is not one the statement can take.
259    pub fn syntax(message: impl Into<String>) -> Self {
260        Self::new(ErrorCode::Syntax, message)
261    }
262
263    /// The text is SQL and it does not mean anything.
264    pub fn binder(message: impl Into<String>) -> Self {
265        Self::new(ErrorCode::Binder, message)
266    }
267
268    /// A named object is missing, or one that should be missing is not.
269    pub fn catalog(message: impl Into<String>) -> Self {
270        Self::new(ErrorCode::Catalog, message)
271    }
272
273    /// A value will not convert to the type it is being asked for.
274    pub fn conversion(message: impl Into<String>) -> Self {
275        Self::new(ErrorCode::Conversion, message)
276    }
277
278    /// A value is outside what its type can hold.
279    pub fn out_of_range(message: impl Into<String>) -> Self {
280        Self::new(ErrorCode::OutOfRange, message)
281    }
282
283    /// An argument is wrong in a way that is not a type error.
284    pub fn invalid_input(message: impl Into<String>) -> Self {
285        Self::new(ErrorCode::InvalidInput, message)
286    }
287
288    /// An allocation failed or a memory limit was reached.
289    pub fn out_of_memory(message: impl Into<String>) -> Self {
290        Self::new(ErrorCode::OutOfMemory, message)
291    }
292
293    /// The filesystem, the network or the object store said no.
294    pub fn io(message: impl Into<String>) -> Self {
295        Self::new(ErrorCode::Io, message)
296    }
297
298    /// It is in the plan and it is not built yet.
299    pub fn not_implemented(message: impl Into<String>) -> Self {
300        Self::new(ErrorCode::NotImplemented, message)
301    }
302
303    /// A constraint was violated.
304    pub fn constraint(message: impl Into<String>) -> Self {
305        Self::new(ErrorCode::Constraint, message)
306    }
307
308    /// A conflict, an abort, or a statement issued outside a transaction that needs one.
309    pub fn transaction(message: impl Into<String>) -> Self {
310        Self::new(ErrorCode::Transaction, message)
311    }
312
313    /// A setting cannot be applied in the current engine configuration.
314    pub fn settings(message: impl Into<String>) -> Self {
315        Self::new(ErrorCode::Settings, message)
316    }
317
318    /// A value the function refuses outright.
319    pub fn parameter_not_allowed(message: impl Into<String>) -> Self {
320        Self::new(ErrorCode::ParameterNotAllowed, message)
321    }
322
323    /// An entry that others depend on.
324    pub fn dependency(message: impl Into<String>) -> Self {
325        Self::new(ErrorCode::Dependency, message)
326    }
327
328    /// A sequence that cannot give what was asked of it.
329    pub fn sequence(message: impl Into<String>) -> Self {
330        Self::new(ErrorCode::Sequence, message)
331    }
332
333    /// Two types that cannot meet.
334    pub fn mismatch_type(message: impl Into<String>) -> Self {
335        Self::new(ErrorCode::MismatchType, message)
336    }
337
338    /// A type used where it cannot be.
339    pub fn invalid_type(message: impl Into<String>) -> Self {
340        Self::new(ErrorCode::InvalidType, message)
341    }
342
343    /// The query was cancelled.
344    pub fn interrupt(message: impl Into<String>) -> Self {
345        Self::new(ErrorCode::Interrupt, message)
346    }
347
348    /// An invariant this code is responsible for does not hold.
349    ///
350    /// Reaching this is always a bug in the database and never a bug in the query, which is why it
351    /// reads differently from the others and why the fuzzer treats it as a finding.
352    /// Something another holder already has, such as a file attached under another name.
353    pub fn resource_in_use(message: impl Into<String>) -> Self {
354        Self::new(ErrorCode::ResourceInUse, message)
355    }
356
357    pub fn internal(message: impl Into<String>) -> Self {
358        Self::new(ErrorCode::Internal, message)
359    }
360}
361
362impl fmt::Display for Error {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        if self.0.message_only {
365            return f.write_str(&self.0.message);
366        }
367        write!(f, "{}: {}", self.0.code, self.0.message)
368    }
369}
370
371impl ErrorCode {
372    const fn json_name(self) -> &'static str {
373        match self {
374            Self::Parser => "Parser",
375            Self::Syntax => "Syntax",
376            Self::Binder => "Binder",
377            Self::Catalog => "Catalog",
378            Self::Conversion => "Conversion",
379            Self::OutOfRange => "Out of Range",
380            Self::InvalidInput => "Invalid Input",
381            Self::OutOfMemory => "Out of Memory",
382            Self::Io => "IO",
383            Self::NotImplemented => "Not implemented",
384            Self::Constraint => "Constraint",
385            Self::Dependency => "Dependency",
386            Self::Sequence => "Sequence",
387            Self::Transaction => "TransactionContext",
388            Self::Settings => "Settings",
389            Self::Interrupt => "Interrupt",
390            Self::ParameterNotAllowed => "Parameter Not Allowed",
391            Self::MismatchType => "Mismatch Type",
392            Self::InvalidType => "Invalid type",
393            Self::ResourceInUse => "Resource In Use",
394            Self::Internal => "INTERNAL",
395        }
396    }
397
398    fn json_subtype(self, message: &str) -> Option<&'static str> {
399        match self {
400            Self::Parser => Some("SYNTAX_ERROR"),
401            Self::Binder if message.starts_with("Referenced column") => Some("COLUMN_NOT_FOUND"),
402            Self::Binder if message.contains("No function matches") => Some("NO_MATCHING_FUNCTION"),
403            Self::Catalog if message.contains("does not exist") => Some("MISSING_ENTRY"),
404            _ => None,
405        }
406    }
407}
408
409fn json_object(fields: &[(&str, String)]) -> String {
410    let mut out = String::from("{");
411    for (index, (name, value)) in fields.iter().enumerate() {
412        if index != 0 {
413            out.push(',');
414        }
415        out.push('"');
416        out.push_str(name);
417        out.push_str("\":\"");
418        for character in value.chars() {
419            match character {
420                '"' => out.push_str("\\\""),
421                '\\' => out.push_str("\\\\"),
422                '\n' => out.push_str("\\n"),
423                '\r' => out.push_str("\\r"),
424                '\t' => out.push_str("\\t"),
425                character if character <= '\u{1f}' => {
426                    use std::fmt::Write as _;
427                    let _ = write!(out, "\\u{:04x}", character as u32);
428                }
429                character => out.push(character),
430            }
431        }
432        out.push('"');
433    }
434    out.push('}');
435    out
436}
437
438impl std::error::Error for Error {}
439
440impl From<std::io::Error> for Error {
441    fn from(error: std::io::Error) -> Self {
442        Self::io(error.to_string())
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::{Error, ErrorCode, Span};
449
450    #[test]
451    fn an_error_prints_the_way_duckdb_prints_it() {
452        let error = Error::binder("Referenced column \"nope\" not found in FROM clause!");
453        assert_eq!(
454            error.to_string(),
455            "Binder Error: Referenced column \"nope\" not found in FROM clause!"
456        );
457    }
458
459    #[test]
460    fn a_json_error_is_structured_and_has_no_text_prefix() {
461        let error = Error::binder("Referenced column \"nope\" not found\nnext")
462            .with_span(Span::new(7, 11))
463            .into_json();
464        assert_eq!(
465            error.to_string(),
466            "{\"exception_type\":\"Binder\",\"exception_message\":\"Referenced column \\\"nope\\\" not found\\nnext\",\"location\":\"[7,4]\",\"position\":\"7\",\"error_subtype\":\"COLUMN_NOT_FOUND\"}"
467        );
468        assert_eq!(error.code(), ErrorCode::Binder);
469    }
470
471    #[test]
472    fn a_result_is_no_wider_than_the_value_in_it() {
473        // The reason the payload is boxed. If this ever fails, every function in the workspace
474        // got more expensive to return from and nobody noticed.
475        assert_eq!(size_of::<Error>(), size_of::<usize>());
476        assert_eq!(size_of::<Result<String, Error>>(), size_of::<String>());
477    }
478
479    #[test]
480    fn a_span_survives_being_attached() {
481        let error = Error::parser("syntax error at or near \"FROM\"").with_span(Span::new(7, 11));
482        assert_eq!(error.span(), Some(Span::new(7, 11)));
483        assert_eq!(error.span().map(Span::len), Some(4));
484        assert_eq!(error.code(), ErrorCode::Parser);
485    }
486
487    #[test]
488    fn a_fallback_span_keeps_the_more_specific_range() {
489        let specific = Error::binder("missing")
490            .with_span(Span::new(7, 14))
491            .with_fallback_span(Span::new(0, 20));
492        assert_eq!(specific.span(), Some(Span::new(7, 14)));
493        let fallback = Error::binder("missing").with_fallback_span(Span::new(0, 20));
494        assert_eq!(fallback.span(), Some(Span::new(0, 20)));
495        assert_eq!(Error::binder("missing").with_fallback_span(Span::new(0, 0)).span(), None);
496    }
497
498    #[test]
499    fn the_fuzzer_can_tell_our_bugs_from_the_query_s_bugs() {
500        assert!(ErrorCode::Binder.is_user_error());
501        assert!(ErrorCode::Conversion.is_user_error());
502        assert!(!ErrorCode::Internal.is_user_error());
503        assert!(!ErrorCode::OutOfMemory.is_user_error());
504        // Not implemented is ours rather than the query's, because the query was legitimate and we
505        // are the reason it did not run.
506        assert!(!ErrorCode::NotImplemented.is_user_error());
507    }
508}