use std::fmt;
use std::panic::Location;
use crate::sqlstate::SqlState;
#[derive(Clone, Debug)]
pub struct Error {
state: SqlState,
message: String,
position: Option<usize>,
place: &'static Location<'static>,
more: Option<Box<More>>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct More {
detail: Option<String>,
hint: Option<String>,
context: Option<String>,
notice: bool,
level: Option<Level>,
internal: Option<(String, Option<usize>)>,
objects: [Option<String>; 5],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Object {
Schema,
Table,
Column,
Datatype,
Constraint,
}
impl Object {
pub const ALL: [Object; 5] =
[Object::Schema, Object::Table, Object::Column, Object::Datatype, Object::Constraint];
pub fn code(self) -> u8 {
match self {
Object::Schema => b's',
Object::Table => b't',
Object::Column => b'c',
Object::Datatype => b'd',
Object::Constraint => b'n',
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Debug,
Log,
Info,
Notice,
Warning,
}
impl Level {
pub fn name(self) -> &'static str {
match self {
Level::Debug => "DEBUG",
Level::Log => "LOG",
Level::Info => "INFO",
Level::Notice => "NOTICE",
Level::Warning => "WARNING",
}
}
}
impl Error {
#[track_caller]
pub fn new(state: SqlState, message: impl Into<String>) -> Error {
Error {
state,
message: message.into(),
position: None,
place: Location::caller(),
more: None,
}
}
#[track_caller]
pub fn corrupted(message: impl Into<String>) -> Error {
Error::new(SqlState::DATA_CORRUPTED, message)
}
#[track_caller]
pub fn internal(message: impl Into<String>) -> Error {
Error::new(SqlState::INTERNAL_ERROR, message)
}
#[must_use]
pub fn with_detail(mut self, detail: impl Into<String>) -> Error {
self.more().detail = Some(detail.into());
self
}
#[must_use]
pub fn with_hint(mut self, hint: impl Into<String>) -> Error {
self.more().hint = Some(hint.into());
self
}
#[must_use]
pub fn at(mut self, location: usize) -> Error {
self.position.get_or_insert(location);
self
}
#[must_use]
pub fn with_position(mut self, location: Option<usize>) -> Error {
self.position = location;
self
}
#[must_use]
pub fn with_context(mut self, line: impl Into<String>) -> Error {
let line = line.into();
let more = self.more();
more.context = Some(match more.context.take() {
Some(context) => format!("{context}\n{line}"),
None => line,
});
self
}
#[must_use]
pub fn as_notice(mut self) -> Error {
self.more().notice = true;
self
}
pub fn is_notice(&self) -> bool {
self.more.as_ref().is_some_and(|more| more.notice)
}
#[must_use]
pub fn with_level(mut self, level: Level) -> Error {
self.more().level = Some(level);
self
}
pub fn level(&self) -> Option<Level> {
self.more.as_ref()?.level
}
#[must_use]
pub fn with_internal(mut self, query: impl Into<String>, position: Option<usize>) -> Error {
self.more().internal = Some((query.into(), position));
self
}
pub fn internal_query(&self) -> Option<(&str, Option<usize>)> {
let (query, position) = self.more.as_ref()?.internal.as_ref()?;
Some((query, *position))
}
#[must_use]
pub fn with_object(mut self, object: Object, name: impl Into<String>) -> Error {
self.more().objects[object as usize] = Some(name.into());
self
}
pub fn object(&self, object: Object) -> Option<&str> {
self.more.as_ref()?.objects[object as usize].as_deref()
}
fn more(&mut self) -> &mut More {
self.more.get_or_insert_with(Box::default)
}
pub fn state(&self) -> SqlState {
self.state
}
pub fn message(&self) -> &str {
&self.message
}
pub fn detail(&self) -> Option<&str> {
self.more.as_ref()?.detail.as_deref()
}
pub fn hint(&self) -> Option<&str> {
self.more.as_ref()?.hint.as_deref()
}
pub fn position(&self) -> Option<usize> {
self.position
}
pub fn context(&self) -> Option<&str> {
self.more.as_ref()?.context.as_deref()
}
pub fn place(&self) -> &'static Location<'static> {
self.place
}
}
impl PartialEq for Error {
fn eq(&self, other: &Error) -> bool {
(self.state, &self.message, self.position, &self.more)
== (other.state, &other.message, other.position, &other.more)
}
}
impl Eq for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.state, self.message)?;
if let Some(detail) = self.detail() {
write!(f, "\nDETAIL: {detail}")?;
}
if let Some(hint) = self.hint() {
write!(f, "\nHINT: {hint}")?;
}
Ok(())
}
}
impl std::error::Error for Error {}
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[cfg(test)]
mod tests {
use super::{Error, Object};
#[test]
fn display() {
let e = Error::corrupted("bad checksum").with_detail("page 7").with_hint("run rupg verify");
assert_eq!(e.to_string(), "XX001: bad checksum\nDETAIL: page 7\nHINT: run rupg verify");
assert_eq!((e.position(), e.clone().at(7).at(9).position()), (None, Some(7)));
}
#[test]
fn the_place_is_where_the_error_is_made() {
let (e, line) = (Error::internal("bad state"), line!());
assert_eq!((e.place().file(), e.place().line()), (file!(), line));
assert_eq!(e, Error::internal("bad state"));
}
#[test]
fn the_names_of_the_object() {
let e =
Error::internal("x").with_object(Object::Table, "t").with_object(Object::Schema, "s");
let names: Vec<_> = Object::ALL.iter().map(|&o| e.object(o)).collect();
assert_eq!(names, [Some("s"), Some("t"), None, None, None]);
assert_ne!(e, Error::internal("x"));
}
}