use rusqlite::ffi;
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorOperation {
Open,
Prepare,
Read,
Write,
Commit,
Rollback,
Publish,
Check,
Repair,
Recover,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationOutcome {
NotApplied,
Pending,
Committed,
Unknown,
}
#[derive(Clone, Copy, Debug, Default, serde::Serialize)]
struct State {
operation: Option<ErrorOperation>,
outcome: Option<OperationOutcome>,
in_transaction: Option<bool>,
connection_usable: Option<bool>,
}
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
pub struct Error {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
code: &'static str,
state: State,
cleanup: Vec<Error>,
}
impl Error {
pub fn new(message: String) -> Self {
Self {
message,
source: None,
code: "BOXDB_VALIDATION",
state: State::default(),
cleanup: vec![],
}
}
pub(crate) fn sqlite(code: i32, message: String) -> Self {
rusqlite::Error::SqliteFailure(ffi::Error::new(code), Some(message)).into()
}
pub fn with_context(mut self, context: impl std::fmt::Display) -> Self {
self.message = format!("{context}: {}", self.message);
self
}
pub fn code(&self) -> &'static str {
if self.extended_code() == Some(ffi::SQLITE_CONSTRAINT_TRIGGER)
&& let Some(rusqlite::Error::SqliteFailure(_, Some(message))) = self
.source
.as_ref()
.and_then(|source| source.downcast_ref::<rusqlite::Error>())
&& message.starts_with(crate::schema::readonly::MESSAGE)
{
return "BOXDB_CONSTRAINT_READONLY";
}
self.extended_code()
.map(result_code_name)
.unwrap_or(self.code)
}
pub fn result_code(&self) -> Option<i32> {
self.extended_code().map(|code| code & 0xff)
}
pub fn extended_code(&self) -> Option<i32> {
match self.source.as_ref()?.downcast_ref::<rusqlite::Error>()? {
rusqlite::Error::SqliteFailure(error, _) => Some(error.extended_code),
rusqlite::Error::SqlInputError { error, .. } => Some(error.extended_code),
_ => None,
}
}
pub fn operation(&self) -> Option<ErrorOperation> {
self.state.operation
}
pub fn outcome(&self) -> Option<OperationOutcome> {
self.state.outcome
}
pub fn in_transaction(&self) -> Option<bool> {
self.state.in_transaction
}
pub fn connection_usable(&self) -> Option<bool> {
self.state.connection_usable
}
pub fn cleanup_errors(&self) -> &[Error] {
&self.cleanup
}
pub(crate) fn state(
mut self,
operation: ErrorOperation,
outcome: OperationOutcome,
in_transaction: bool,
usable: bool,
) -> Self {
if self.state.operation.is_none() {
self.state.operation = Some(operation);
}
if self.state.outcome.is_none() {
self.state.outcome = Some(outcome);
}
self.state.in_transaction = Some(in_transaction);
self.state.connection_usable = Some(usable);
self
}
pub(crate) fn cleanup(mut self, error: Error) -> Self {
self.cleanup.push(error);
self
}
pub(crate) fn script_failure(mut self) -> Self {
self.state.outcome = Some(OperationOutcome::Unknown);
self
}
pub(crate) fn fatal(&self) -> bool {
matches!(
self.result_code(),
Some(ffi::SQLITE_CORRUPT | ffi::SQLITE_NOTADB)
)
}
}
impl Error {
pub fn validation(error: impl std::error::Error + Send + Sync + 'static) -> Self {
Self::from_source(error, "BOXDB_VALIDATION")
}
pub(crate) fn with_source(
mut self,
error: impl std::error::Error + Send + Sync + 'static,
) -> Self {
self.source = Some(Box::new(error));
self
}
fn from_source(
error: impl std::error::Error + Send + Sync + 'static,
code: &'static str,
) -> Self {
Self {
message: error.to_string(),
source: Some(Box::new(error)),
code,
state: State::default(),
cleanup: vec![],
}
}
pub(crate) fn with_code(mut self, code: &'static str) -> Self {
self.code = code;
self
}
pub fn internal(message: impl Into<String>) -> Self {
Self::new(message.into()).with_code("BOXDB_INTERNAL")
}
pub fn closed(message: impl Into<String>) -> Self {
Self::new(message.into()).with_code("BOXDB_CLOSED")
}
}
impl From<rusqlite::Error> for Error {
fn from(error: rusqlite::Error) -> Self {
let code = match error {
rusqlite::Error::InvalidParameterName(_)
| rusqlite::Error::InvalidParameterCount(..)
| rusqlite::Error::InvalidQuery
| rusqlite::Error::MultipleStatement
| rusqlite::Error::InvalidPath(_)
| rusqlite::Error::NulError(_) => "BOXDB_VALIDATION",
_ => "BOXDB_ERROR",
};
Self::from_source(error, code)
}
}
impl serde::Serialize for Error {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
#[derive(serde::Serialize)]
struct Details<'a> {
message: &'a str,
code: &'static str,
result_code: Option<i32>,
extended_code: Option<i32>,
#[serde(flatten)]
state: State,
cleanup: &'a [Error],
}
Details {
message: &self.message,
code: self.code(),
result_code: self.result_code(),
extended_code: self.extended_code(),
state: self.state,
cleanup: &self.cleanup,
}
.serialize(serializer)
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::from_source(error, "BOXDB_IO")
}
}
impl From<serde_saphyr::SerializeError> for Error {
fn from(error: serde_saphyr::SerializeError) -> Self {
Self::from_source(error, "BOXDB_INTERNAL")
}
}
macro_rules! convert_error {
($($ty:ty),*) => {$(impl From<$ty> for Error {
fn from(error: $ty) -> Self { Self::from_source(error, "BOXDB_VALIDATION") }
})*};
}
convert_error!(
serde_json::Error,
serde_saphyr::DeserializeError,
sqlparser::parser::ParserError
);
fn result_code_name(code: i32) -> &'static str {
macro_rules! names {
($($name:ident => $suffix:literal),* $(,)?) => {
match code {
$(ffi::$name => concat!("BOXDB_", $suffix),)*
_ if code != code & 0xff => result_code_name(code & 0xff),
_ => "BOXDB_ERROR",
}
};
}
names!(
SQLITE_ERROR => "ERROR",
SQLITE_INTERNAL => "INTERNAL",
SQLITE_PERM => "PERM",
SQLITE_ABORT => "ABORT",
SQLITE_BUSY => "BUSY",
SQLITE_LOCKED => "LOCKED",
SQLITE_NOMEM => "NOMEM",
SQLITE_READONLY => "READONLY",
SQLITE_INTERRUPT => "INTERRUPT",
SQLITE_IOERR => "IOERR",
SQLITE_CORRUPT => "CORRUPT",
SQLITE_NOTFOUND => "NOTFOUND",
SQLITE_FULL => "FULL",
SQLITE_CANTOPEN => "CANTOPEN",
SQLITE_PROTOCOL => "PROTOCOL",
SQLITE_EMPTY => "EMPTY",
SQLITE_SCHEMA => "SCHEMA",
SQLITE_TOOBIG => "TOOBIG",
SQLITE_CONSTRAINT => "CONSTRAINT",
SQLITE_MISMATCH => "MISMATCH",
SQLITE_MISUSE => "MISUSE",
SQLITE_NOLFS => "NOLFS",
SQLITE_AUTH => "AUTH",
SQLITE_FORMAT => "FORMAT",
SQLITE_RANGE => "RANGE",
SQLITE_NOTADB => "NOTADB",
SQLITE_NOTICE => "NOTICE",
SQLITE_WARNING => "WARNING",
SQLITE_ERROR_MISSING_COLLSEQ => "ERROR_MISSING_COLLSEQ",
SQLITE_ERROR_RETRY => "ERROR_RETRY",
SQLITE_ERROR_SNAPSHOT => "ERROR_SNAPSHOT",
SQLITE_ERROR_RESERVESIZE => "ERROR_RESERVESIZE",
SQLITE_ERROR_KEY => "ERROR_KEY",
SQLITE_ERROR_UNABLE => "ERROR_UNABLE",
SQLITE_IOERR_READ => "IOERR_READ",
SQLITE_IOERR_SHORT_READ => "IOERR_SHORT_READ",
SQLITE_IOERR_WRITE => "IOERR_WRITE",
SQLITE_IOERR_FSYNC => "IOERR_FSYNC",
SQLITE_IOERR_DIR_FSYNC => "IOERR_DIR_FSYNC",
SQLITE_IOERR_TRUNCATE => "IOERR_TRUNCATE",
SQLITE_IOERR_FSTAT => "IOERR_FSTAT",
SQLITE_IOERR_UNLOCK => "IOERR_UNLOCK",
SQLITE_IOERR_RDLOCK => "IOERR_RDLOCK",
SQLITE_IOERR_DELETE => "IOERR_DELETE",
SQLITE_IOERR_BLOCKED => "IOERR_BLOCKED",
SQLITE_IOERR_NOMEM => "IOERR_NOMEM",
SQLITE_IOERR_ACCESS => "IOERR_ACCESS",
SQLITE_IOERR_CHECKRESERVEDLOCK => "IOERR_CHECKRESERVEDLOCK",
SQLITE_IOERR_LOCK => "IOERR_LOCK",
SQLITE_IOERR_CLOSE => "IOERR_CLOSE",
SQLITE_IOERR_DIR_CLOSE => "IOERR_DIR_CLOSE",
SQLITE_IOERR_SHMOPEN => "IOERR_SHMOPEN",
SQLITE_IOERR_SHMSIZE => "IOERR_SHMSIZE",
SQLITE_IOERR_SHMLOCK => "IOERR_SHMLOCK",
SQLITE_IOERR_SHMMAP => "IOERR_SHMMAP",
SQLITE_IOERR_SEEK => "IOERR_SEEK",
SQLITE_IOERR_DELETE_NOENT => "IOERR_DELETE_NOENT",
SQLITE_IOERR_MMAP => "IOERR_MMAP",
SQLITE_IOERR_GETTEMPPATH => "IOERR_GETTEMPPATH",
SQLITE_IOERR_CONVPATH => "IOERR_CONVPATH",
SQLITE_IOERR_VNODE => "IOERR_VNODE",
SQLITE_IOERR_AUTH => "IOERR_AUTH",
SQLITE_IOERR_BEGIN_ATOMIC => "IOERR_BEGIN_ATOMIC",
SQLITE_IOERR_COMMIT_ATOMIC => "IOERR_COMMIT_ATOMIC",
SQLITE_IOERR_ROLLBACK_ATOMIC => "IOERR_ROLLBACK_ATOMIC",
SQLITE_IOERR_DATA => "IOERR_DATA",
SQLITE_IOERR_CORRUPTFS => "IOERR_CORRUPTFS",
SQLITE_IOERR_IN_PAGE => "IOERR_IN_PAGE",
SQLITE_IOERR_BADKEY => "IOERR_BADKEY",
SQLITE_IOERR_CODEC => "IOERR_CODEC",
SQLITE_LOCKED_SHAREDCACHE => "LOCKED_SHAREDCACHE",
SQLITE_LOCKED_VTAB => "LOCKED_VTAB",
SQLITE_BUSY_RECOVERY => "BUSY_RECOVERY",
SQLITE_BUSY_SNAPSHOT => "BUSY_SNAPSHOT",
SQLITE_BUSY_TIMEOUT => "BUSY_TIMEOUT",
SQLITE_CANTOPEN_NOTEMPDIR => "CANTOPEN_NOTEMPDIR",
SQLITE_CANTOPEN_ISDIR => "CANTOPEN_ISDIR",
SQLITE_CANTOPEN_FULLPATH => "CANTOPEN_FULLPATH",
SQLITE_CANTOPEN_CONVPATH => "CANTOPEN_CONVPATH",
SQLITE_CANTOPEN_DIRTYWAL => "CANTOPEN_DIRTYWAL",
SQLITE_CANTOPEN_SYMLINK => "CANTOPEN_SYMLINK",
SQLITE_CORRUPT_VTAB => "CORRUPT_VTAB",
SQLITE_CORRUPT_SEQUENCE => "CORRUPT_SEQUENCE",
SQLITE_CORRUPT_INDEX => "CORRUPT_INDEX",
SQLITE_READONLY_RECOVERY => "READONLY_RECOVERY",
SQLITE_READONLY_CANTLOCK => "READONLY_CANTLOCK",
SQLITE_READONLY_ROLLBACK => "READONLY_ROLLBACK",
SQLITE_READONLY_DBMOVED => "READONLY_DBMOVED",
SQLITE_READONLY_CANTINIT => "READONLY_CANTINIT",
SQLITE_READONLY_DIRECTORY => "READONLY_DIRECTORY",
SQLITE_ABORT_ROLLBACK => "ABORT_ROLLBACK",
SQLITE_CONSTRAINT_CHECK => "CONSTRAINT_CHECK",
SQLITE_CONSTRAINT_COMMITHOOK => "CONSTRAINT_COMMITHOOK",
SQLITE_CONSTRAINT_FOREIGNKEY => "CONSTRAINT_FOREIGNKEY",
SQLITE_CONSTRAINT_FUNCTION => "CONSTRAINT_FUNCTION",
SQLITE_CONSTRAINT_NOTNULL => "CONSTRAINT_NOTNULL",
SQLITE_CONSTRAINT_PRIMARYKEY => "CONSTRAINT_PRIMARYKEY",
SQLITE_CONSTRAINT_TRIGGER => "CONSTRAINT_TRIGGER",
SQLITE_CONSTRAINT_UNIQUE => "CONSTRAINT_UNIQUE",
SQLITE_CONSTRAINT_VTAB => "CONSTRAINT_VTAB",
SQLITE_CONSTRAINT_ROWID => "CONSTRAINT_ROWID",
SQLITE_CONSTRAINT_PINNED => "CONSTRAINT_PINNED",
SQLITE_CONSTRAINT_DATATYPE => "CONSTRAINT_DATATYPE",
SQLITE_NOTICE_RECOVER_WAL => "NOTICE_RECOVER_WAL",
SQLITE_NOTICE_RECOVER_ROLLBACK => "NOTICE_RECOVER_ROLLBACK",
SQLITE_NOTICE_RBU => "NOTICE_RBU",
SQLITE_WARNING_AUTOINDEX => "WARNING_AUTOINDEX",
SQLITE_AUTH_USER => "AUTH_USER",
)
}