use std::fmt;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Span {
pub start: u32,
pub end: u32,
}
impl Span {
#[must_use]
pub const fn new(start: u32, end: u32) -> Self {
Self { start, end }
}
#[must_use]
pub const fn len(self) -> u32 {
self.end.saturating_sub(self.start)
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorCode {
Parser,
Syntax,
Binder,
Catalog,
Conversion,
OutOfRange,
InvalidInput,
OutOfMemory,
Io,
NotImplemented,
Constraint,
Transaction,
Settings,
Interrupt,
Internal,
}
impl ErrorCode {
#[must_use]
pub const fn duckdb_name(self) -> &'static str {
match self {
Self::Parser => "Parser Error",
Self::Syntax => "Syntax Error",
Self::Binder => "Binder Error",
Self::Catalog => "Catalog Error",
Self::Conversion => "Conversion Error",
Self::OutOfRange => "Out of Range Error",
Self::InvalidInput => "Invalid Input Error",
Self::OutOfMemory => "Out of Memory Error",
Self::Io => "IO Error",
Self::NotImplemented => "Not implemented Error",
Self::Constraint => "Constraint Error",
Self::Transaction => "TransactionContext Error",
Self::Settings => "Settings Error",
Self::Interrupt => "Interrupt Error",
Self::Internal => "INTERNAL Error",
}
}
#[must_use]
pub const fn is_user_error(self) -> bool {
matches!(
self,
Self::Parser
| Self::Syntax
| Self::Binder
| Self::Catalog
| Self::Conversion
| Self::OutOfRange
| Self::InvalidInput
| Self::Constraint
| Self::Transaction
| Self::Settings
)
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.duckdb_name())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error(Box<Payload>);
#[derive(Debug, Clone, PartialEq, Eq)]
struct Payload {
code: ErrorCode,
message: String,
span: Option<Span>,
message_only: bool,
}
impl Error {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self(Box::new(Payload { code, message: message.into(), span: None, message_only: false }))
}
#[must_use]
pub fn with_span(mut self, span: Span) -> Self {
self.0.span = Some(span);
self
}
#[must_use]
pub fn with_fallback_span(mut self, span: Span) -> Self {
if self.0.span.is_none() && !span.is_empty() {
self.0.span = Some(span);
}
self
}
#[must_use]
pub fn code(&self) -> ErrorCode {
self.0.code
}
#[must_use]
pub fn message(&self) -> &str {
&self.0.message
}
#[must_use]
pub fn span(&self) -> Option<Span> {
self.0.span
}
#[must_use]
pub fn into_json(mut self) -> Self {
let exception_type = self.0.code.json_name();
let subtype = self.0.code.json_subtype(&self.0.message);
let mut fields = vec![
("exception_type", exception_type.to_string()),
("exception_message", self.0.message.clone()),
];
if let Some(span) = self.0.span {
fields.push(("location", format!("[{},{}]", span.start, span.len())));
fields.push(("position", span.start.to_string()));
}
if let Some(subtype) = subtype {
fields.push(("error_subtype", subtype.to_string()));
}
self.0.message = json_object(&fields);
self.0.message_only = true;
self
}
pub fn parser(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Parser, message)
}
pub fn syntax(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Syntax, message)
}
pub fn binder(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Binder, message)
}
pub fn catalog(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Catalog, message)
}
pub fn conversion(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Conversion, message)
}
pub fn out_of_range(message: impl Into<String>) -> Self {
Self::new(ErrorCode::OutOfRange, message)
}
pub fn invalid_input(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidInput, message)
}
pub fn out_of_memory(message: impl Into<String>) -> Self {
Self::new(ErrorCode::OutOfMemory, message)
}
pub fn io(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Io, message)
}
pub fn not_implemented(message: impl Into<String>) -> Self {
Self::new(ErrorCode::NotImplemented, message)
}
pub fn constraint(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Constraint, message)
}
pub fn transaction(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Transaction, message)
}
pub fn settings(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Settings, message)
}
pub fn interrupt(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Interrupt, message)
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Internal, message)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.message_only {
return f.write_str(&self.0.message);
}
write!(f, "{}: {}", self.0.code, self.0.message)
}
}
impl ErrorCode {
const fn json_name(self) -> &'static str {
match self {
Self::Parser => "Parser",
Self::Syntax => "Syntax",
Self::Binder => "Binder",
Self::Catalog => "Catalog",
Self::Conversion => "Conversion",
Self::OutOfRange => "Out of Range",
Self::InvalidInput => "Invalid Input",
Self::OutOfMemory => "Out of Memory",
Self::Io => "IO",
Self::NotImplemented => "Not implemented",
Self::Constraint => "Constraint",
Self::Transaction => "TransactionContext",
Self::Settings => "Settings",
Self::Interrupt => "Interrupt",
Self::Internal => "INTERNAL",
}
}
fn json_subtype(self, message: &str) -> Option<&'static str> {
match self {
Self::Parser => Some("SYNTAX_ERROR"),
Self::Binder if message.starts_with("Referenced column") => Some("COLUMN_NOT_FOUND"),
Self::Binder if message.contains("No function matches") => Some("NO_MATCHING_FUNCTION"),
Self::Catalog if message.contains("does not exist") => Some("MISSING_ENTRY"),
_ => None,
}
}
}
fn json_object(fields: &[(&str, String)]) -> String {
let mut out = String::from("{");
for (index, (name, value)) in fields.iter().enumerate() {
if index != 0 {
out.push(',');
}
out.push('"');
out.push_str(name);
out.push_str("\":\"");
for character in value.chars() {
match character {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
character if character <= '\u{1f}' => {
use std::fmt::Write as _;
let _ = write!(out, "\\u{:04x}", character as u32);
}
character => out.push(character),
}
}
out.push('"');
}
out.push('}');
out
}
impl std::error::Error for Error {}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::io(error.to_string())
}
}
#[cfg(test)]
mod tests {
use super::{Error, ErrorCode, Span};
#[test]
fn an_error_prints_the_way_duckdb_prints_it() {
let error = Error::binder("Referenced column \"nope\" not found in FROM clause!");
assert_eq!(
error.to_string(),
"Binder Error: Referenced column \"nope\" not found in FROM clause!"
);
}
#[test]
fn a_json_error_is_structured_and_has_no_text_prefix() {
let error = Error::binder("Referenced column \"nope\" not found\nnext")
.with_span(Span::new(7, 11))
.into_json();
assert_eq!(
error.to_string(),
"{\"exception_type\":\"Binder\",\"exception_message\":\"Referenced column \\\"nope\\\" not found\\nnext\",\"location\":\"[7,4]\",\"position\":\"7\",\"error_subtype\":\"COLUMN_NOT_FOUND\"}"
);
assert_eq!(error.code(), ErrorCode::Binder);
}
#[test]
fn a_result_is_no_wider_than_the_value_in_it() {
assert_eq!(size_of::<Error>(), size_of::<usize>());
assert_eq!(size_of::<Result<String, Error>>(), size_of::<String>());
}
#[test]
fn a_span_survives_being_attached() {
let error = Error::parser("syntax error at or near \"FROM\"").with_span(Span::new(7, 11));
assert_eq!(error.span(), Some(Span::new(7, 11)));
assert_eq!(error.span().map(Span::len), Some(4));
assert_eq!(error.code(), ErrorCode::Parser);
}
#[test]
fn a_fallback_span_keeps_the_more_specific_range() {
let specific = Error::binder("missing")
.with_span(Span::new(7, 14))
.with_fallback_span(Span::new(0, 20));
assert_eq!(specific.span(), Some(Span::new(7, 14)));
let fallback = Error::binder("missing").with_fallback_span(Span::new(0, 20));
assert_eq!(fallback.span(), Some(Span::new(0, 20)));
assert_eq!(Error::binder("missing").with_fallback_span(Span::new(0, 0)).span(), None);
}
#[test]
fn the_fuzzer_can_tell_our_bugs_from_the_query_s_bugs() {
assert!(ErrorCode::Binder.is_user_error());
assert!(ErrorCode::Conversion.is_user_error());
assert!(!ErrorCode::Internal.is_user_error());
assert!(!ErrorCode::OutOfMemory.is_user_error());
assert!(!ErrorCode::NotImplemented.is_user_error());
}
}