#![allow(clippy::doc_markdown)]
use std::sync::OnceLock;
use rusqlite::{
Error,
ffi::{
self as sys, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_NOTNULL,
SQLITE_CONSTRAINT_PRIMARYKEY, SQLITE_CONSTRAINT_TRIGGER,
SQLITE_CONSTRAINT_UNIQUE
}
};
#[allow(clippy::type_complexity)]
static SQLERR_INTERPRETER: OnceLock<
Box<dyn Fn(&InterpretedError) -> Option<String> + 'static + Send + Sync>
> = OnceLock::new();
pub fn register_interpreter(
f: impl Fn(&InterpretedError) -> Option<String> + 'static + Send + Sync
) {
let _ = SQLERR_INTERPRETER.set(Box::new(f));
}
#[must_use]
pub fn interpret(err: &rusqlite::Error) -> Option<String> {
deconstruct_error(err).as_ref().and_then(|ie| {
SQLERR_INTERPRETER.get().and_then(|stringify| stringify(ie))
})
}
#[derive(Debug)]
#[non_exhaustive]
pub enum InterpretedError<'e> {
NotUnique(Vec<(&'e str, &'e str)>),
NotNull(Vec<(&'e str, &'e str)>),
Check(&'e str),
ForeignKey
}
#[must_use]
pub fn deconstruct_error(err: &Error) -> Option<InterpretedError<'_>> {
match err {
Error::SqliteFailure(
sys::Error {
code,
extended_code
},
Some(msg)
) if *code == sys::ErrorCode::ConstraintViolation => {
match *extended_code {
SQLITE_CONSTRAINT_UNIQUE | SQLITE_CONSTRAINT_PRIMARYKEY => {
deconstruct_unique(msg)
}
SQLITE_CONSTRAINT_NOTNULL => {
deconstruct_notnull(msg)
}
SQLITE_CONSTRAINT_CHECK => {
deconstruct_check(msg)
}
SQLITE_CONSTRAINT_TRIGGER => deconstruct_constraint(msg),
_ => {
None
}
}
}
_e => {
None
}
}
}
fn deconstruct_unique(msg: &str) -> Option<InterpretedError<'_>> {
let Some((_, ns)) = msg.rsplit_once(':') else {
return None;
};
let mut lst: Vec<(&str, &str)> = ns
.trim()
.split(',')
.map(str::trim)
.filter_map(|e| e.split_once('.'))
.collect();
lst.sort_unstable();
Some(InterpretedError::NotUnique(lst))
}
fn deconstruct_notnull(msg: &str) -> Option<InterpretedError<'_>> {
let Some((_, ns)) = msg.rsplit_once(':') else {
return None;
};
let mut lst: Vec<(&str, &str)> = ns
.trim()
.split(',')
.map(str::trim)
.filter_map(|e| e.split_once('.'))
.collect();
lst.sort_unstable();
Some(InterpretedError::NotNull(lst))
}
fn deconstruct_check(msg: &str) -> Option<InterpretedError<'_>> {
let Some((_, rule)) = msg.rsplit_once(':') else {
return None;
};
Some(InterpretedError::Check(rule.trim()))
}
fn deconstruct_constraint(msg: &str) -> Option<InterpretedError<'_>> {
if msg == "FOREIGN KEY constraint failed" {
Some(InterpretedError::ForeignKey)
} else {
None
}
}