use core::fmt;
mod generated {
#![allow(missing_docs)]
use super::ExtendedCode;
include!(concat!(env!("OUT_DIR"), "/errors_generated.rs"));
}
pub use generated::{
ExtendedRow, PrimaryCode, PrimaryRow, ERROR_TABLE_REFERENCE, ERROR_TABLE_SOURCE, EXTENDED_ROWS,
PRIMARY_ROWS,
};
pub type DbResult<T> = Result<T, DbError>;
static UNRECOGNISED_PRIMARY_ROW: PrimaryRow = PrimaryRow {
code: PrimaryCode::Error,
value: 1,
c_name: "SQLITE_ERROR",
message: "SQL logic error",
connection_usable: true,
statement_resettable: true,
transaction_rolled_back: false,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct ExtendedCode(pub i32);
impl ExtendedCode {
pub fn from_primary(code: PrimaryCode) -> ExtendedCode {
ExtendedCode(code.value())
}
pub fn value(self) -> i32 {
self.0
}
pub fn row(self) -> Option<&'static ExtendedRow> {
EXTENDED_ROWS.iter().find(|row| row.value == self.0)
}
pub fn primary(self) -> PrimaryCode {
match self.row() {
Some(row) => row.primary,
None => PrimaryCode::from_code(self.0 & 0xff).unwrap_or(PrimaryCode::Error),
}
}
pub fn c_name(self) -> Option<&'static str> {
self.row().map(|row| row.c_name)
}
pub fn message(self) -> &'static str {
match self.row() {
Some(row) => row.message,
None => self.primary().message(),
}
}
}
impl PrimaryCode {
pub fn value(self) -> i32 {
self.row().value
}
pub fn row(self) -> &'static PrimaryRow {
match PRIMARY_ROWS.iter().find(|row| row.code == self) {
Some(row) => row,
None => &UNRECOGNISED_PRIMARY_ROW,
}
}
pub fn from_code(number: i32) -> Option<PrimaryCode> {
PRIMARY_ROWS
.iter()
.find(|row| row.value == number)
.map(|row| row.code)
}
pub fn c_name(self) -> &'static str {
self.row().c_name
}
pub fn message(self) -> &'static str {
self.row().message
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct DatabaseName(String);
impl DatabaseName {
pub fn new(name: impl Into<String>) -> DatabaseName {
DatabaseName(name.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for DatabaseName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Unwind {
Statement,
Nothing,
Transaction,
}
#[derive(Clone, Debug, Eq, PartialEq, Default)]
struct ErrorContext {
message: Option<String>,
sql_offset: Option<u32>,
database: Option<DatabaseName>,
detail: Option<String>,
unsupported: Option<String>,
requirement: Option<String>,
unwind: Option<Unwind>,
unwind_explicit: bool,
}
#[derive(Clone, Debug)]
pub struct DbError {
extended: ExtendedCode,
context: Option<Box<ErrorContext>>,
}
impl PartialEq for DbError {
fn eq(&self, other: &DbError) -> bool {
self.extended == other.extended
&& self.message() == other.message()
&& self.sql_offset() == other.sql_offset()
&& self.database() == other.database()
&& self.detail() == other.detail()
&& self.unsupported() == other.unsupported()
&& self.requirement() == other.requirement()
}
}
impl Eq for DbError {}
impl DbError {
fn context_mut(&mut self) -> &mut ErrorContext {
self.context
.get_or_insert_with(Box::<ErrorContext>::default)
}
pub fn new(extended: ExtendedCode) -> DbError {
DbError {
extended,
context: None,
}
}
pub fn primary(code: PrimaryCode) -> DbError {
DbError::new(ExtendedCode::from_primary(code))
}
pub fn with_message(mut self, message: impl Into<String>) -> DbError {
self.context_mut().message = Some(message.into());
self
}
pub fn with_detail(mut self, detail: impl Into<String>) -> DbError {
self.context_mut().detail = Some(detail.into());
self
}
pub fn with_unsupported(mut self, what: impl Into<String>) -> DbError {
self.context_mut().unsupported = Some(what.into());
self
}
pub fn with_requirement(mut self, what: impl Into<String>) -> DbError {
self.context_mut().requirement = Some(what.into());
self
}
pub fn or_unwind(mut self, unwind: Unwind) -> DbError {
let context = self.context_mut();
if context.unwind.is_none() {
context.unwind = Some(unwind);
}
self
}
pub fn with_raised_unwind(mut self, unwind: Unwind) -> DbError {
let context = self.context_mut();
context.unwind = Some(unwind);
context.unwind_explicit = true;
self
}
pub fn with_outer_unwind(mut self, unwind: Unwind) -> DbError {
let context = self.context_mut();
if !context.unwind_explicit {
context.unwind = Some(unwind);
}
self
}
pub fn unwind(&self) -> Unwind {
self.context
.as_ref()
.and_then(|context| context.unwind)
.unwrap_or(Unwind::Statement)
}
pub fn with_sql_offset(mut self, offset: u32) -> DbError {
self.context_mut().sql_offset = Some(offset);
self
}
pub fn with_database(mut self, database: DatabaseName) -> DbError {
self.context_mut().database = Some(database);
self
}
pub fn code(&self) -> PrimaryCode {
self.extended.primary()
}
pub fn extended(&self) -> ExtendedCode {
self.extended
}
pub fn message(&self) -> &str {
match self
.context
.as_ref()
.and_then(|context| context.message.as_deref())
{
Some(message) => message,
None => self.extended.message(),
}
}
pub fn detail(&self) -> Option<&str> {
self.context
.as_ref()
.and_then(|context| context.detail.as_deref())
}
pub fn unsupported(&self) -> Option<&str> {
self.context
.as_ref()
.and_then(|context| context.unsupported.as_deref())
}
pub fn requirement(&self) -> Option<&str> {
self.context
.as_ref()
.and_then(|context| context.requirement.as_deref())
}
pub fn sql_offset(&self) -> Option<u32> {
self.context.as_ref().and_then(|context| context.sql_offset)
}
pub fn database(&self) -> Option<&DatabaseName> {
self.context
.as_ref()
.and_then(|context| context.database.as_ref())
}
pub fn connection_usable(&self) -> bool {
match self.extended.row() {
Some(row) => row.connection_usable,
None => self.code().row().connection_usable,
}
}
pub fn statement_resettable(&self) -> bool {
match self.extended.row() {
Some(row) => row.statement_resettable,
None => self.code().row().statement_resettable,
}
}
pub fn transaction_rolled_back(&self) -> bool {
match self.extended.row() {
Some(row) => row.transaction_rolled_back,
None => self.code().row().transaction_rolled_back,
}
}
}
impl fmt::Display for DbError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.sql_offset() {
Some(offset) => write!(formatter, "{} (at SQL byte {offset})", self.message()),
None => formatter.write_str(self.message()),
}
}
}
impl std::error::Error for DbError {}
pub fn corrupt(detail: impl Into<String>) -> DbError {
DbError::primary(PrimaryCode::Corrupt).with_detail(detail)
}
pub fn too_big(detail: impl Into<String>) -> DbError {
DbError::primary(PrimaryCode::TooBig).with_detail(detail)
}
pub fn no_mem(detail: impl Into<String>) -> DbError {
DbError::primary(PrimaryCode::NoMem).with_detail(detail)
}
pub fn misuse(detail: impl Into<String>) -> DbError {
DbError::primary(PrimaryCode::Misuse).with_detail(detail)
}
pub fn refusal(said: impl Into<String>) -> DbError {
let said = said.into();
DbError::primary(PrimaryCode::Misuse)
.with_message(said.clone())
.with_detail(said)
}
pub fn statement_refusal(said: impl Into<String>) -> DbError {
let said = said.into();
DbError::primary(PrimaryCode::Error)
.with_message(said.clone())
.with_detail(said)
}
pub fn busy(said: impl Into<String>) -> DbError {
let said = said.into();
DbError::primary(PrimaryCode::Busy)
.with_message(said.clone())
.with_detail(said)
}
pub fn unmet_requirement(what: impl Into<String>, said: impl Into<String>) -> DbError {
let said = said.into();
DbError::primary(PrimaryCode::Misuse)
.with_message(said.clone())
.with_detail(said)
.with_requirement(what)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn primary_codes_round_trip_through_their_numeric_value() {
for row in PRIMARY_ROWS.iter() {
assert_eq!(PrimaryCode::from_code(row.value), Some(row.code));
assert_eq!(row.code.value(), row.value);
assert_eq!(row.code.c_name(), row.c_name);
}
}
#[test]
fn extended_codes_agree_with_their_primary_code() {
for row in EXTENDED_ROWS.iter() {
let extended = ExtendedCode(row.value);
assert_eq!(extended.primary(), row.primary);
assert_eq!(row.value & 0xff, row.primary.value());
}
}
#[test]
fn unknown_extended_codes_fall_back_to_their_low_byte() {
let invented = ExtendedCode((99 << 8) | PrimaryCode::Constraint.value());
assert!(invented.row().is_none());
assert_eq!(invented.primary(), PrimaryCode::Constraint);
assert_eq!(invented.message(), PrimaryCode::Constraint.message());
}
#[test]
fn numeric_values_are_unique() {
let mut values: Vec<i32> = PRIMARY_ROWS.iter().map(|row| row.value).collect();
values.extend(EXTENDED_ROWS.iter().map(|row| row.value));
let count = values.len();
values.sort_unstable();
values.dedup();
assert_eq!(
values.len(),
count,
"duplicate result code in compat/errors.toml"
);
}
#[test]
fn display_hides_internal_detail() {
let error = DbError::primary(PrimaryCode::CantOpen)
.with_detail("C:/secret/path/app.db")
.with_sql_offset(12);
let rendered = error.to_string();
assert!(!rendered.contains("secret"), "{rendered}");
assert!(rendered.contains("at SQL byte 12"), "{rendered}");
assert_eq!(error.detail(), Some("C:/secret/path/app.db"));
}
#[test]
fn only_two_of_the_three_misuse_builders_show_their_sentence() {
let said = "embed: no embedding model is installed";
assert_eq!(misuse(said).message(), PrimaryCode::Misuse.message());
assert_eq!(misuse(said).detail(), Some(said));
assert_eq!(refusal(said).message(), said);
assert_eq!(
unmet_requirement("an embedding model", said).message(),
said
);
}
#[test]
fn a_missing_component_is_named_by_a_marker_and_is_not_an_unimplemented_one() {
let error = unmet_requirement(
"an embedding model",
"embed: no embedding model is installed. Run `inillucent setup-embeddings`",
);
assert_eq!(error.requirement(), Some("an embedding model"));
assert_eq!(
error.unsupported(),
None,
"it is built, it is not installed"
);
assert_eq!(error.code(), PrimaryCode::Misuse);
assert!(error.to_string().contains("setup-embeddings"), "{error}");
assert_eq!(
refusal("no such table: peple").requirement(),
None,
"an ordinary statement refusal is missing nothing"
);
}
#[test]
fn recovery_contract_matches_the_manifest() {
assert!(!DbError::primary(PrimaryCode::Misuse).connection_usable());
assert!(DbError::primary(PrimaryCode::Busy).connection_usable());
assert!(DbError::new(ExtendedCode::ABORT_ROLLBACK).transaction_rolled_back());
assert!(!DbError::new(ExtendedCode::CONSTRAINT_UNIQUE).transaction_rolled_back());
}
}