use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum FrankenError {
#[error("database not found: '{path}'")]
DatabaseNotFound { path: PathBuf },
#[error("database is locked: '{path}'")]
DatabaseLocked { path: PathBuf },
#[error("multi-process contract violation: {detail}")]
MultiProcessContractViolation { detail: String },
#[error("database disk image is malformed: {detail}")]
DatabaseCorrupt { detail: String },
#[error("file is not a database: '{path}'")]
NotADatabase { path: PathBuf },
#[error("database is full")]
DatabaseFull,
#[error("database schema has changed")]
SchemaChanged,
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("disk I/O error reading page {page}")]
IoRead { page: u32 },
#[error("disk I/O error writing page {page}")]
IoWrite { page: u32 },
#[error("short read: expected {expected} bytes, got {actual}")]
ShortRead { expected: usize, actual: usize },
#[error("near \"{token}\": syntax error")]
SyntaxError { token: String },
#[error("SQL error at offset {offset}: {detail}")]
ParseError { offset: usize, detail: String },
#[error("query returned no rows")]
QueryReturnedNoRows,
#[error("query returned more than one row")]
QueryReturnedMultipleRows,
#[error("no such table: {name}")]
NoSuchTable { name: String },
#[error("no such column: {name}")]
NoSuchColumn { name: String },
#[error("no such index: {name}")]
NoSuchIndex { name: String },
#[error("table {name} already exists")]
TableExists { name: String },
#[error("index {name} already exists")]
IndexExists { name: String },
#[error("ambiguous column name: {name}")]
AmbiguousColumn { name: String },
#[error("UNIQUE constraint failed: {columns}")]
UniqueViolation { columns: String },
#[error("NOT NULL constraint failed: {column}")]
NotNullViolation { column: String },
#[error("CHECK constraint failed: {name}")]
CheckViolation { name: String },
#[error("FOREIGN KEY constraint failed")]
ForeignKeyViolation,
#[error("PRIMARY KEY constraint failed")]
PrimaryKeyViolation,
#[error("cannot store {actual} value in {column_type} column {column}")]
DatatypeViolation {
column: String,
column_type: String,
actual: String,
},
#[error("cannot start a transaction within a transaction")]
NestedTransaction,
#[error("cannot commit - no transaction is active")]
NoActiveTransaction,
#[error("cannot VACUUM from within a transaction")]
VacuumWithinTransaction,
#[error("transaction rolled back: {reason}")]
TransactionRolledBack { reason: String },
#[error("write conflict on page {page}: held by transaction {holder}")]
WriteConflict { page: u32, holder: u64 },
#[error("serialization failure: page {page} was modified after snapshot")]
SerializationFailure { page: u32 },
#[error("snapshot too old: transaction {txn_id} is below GC horizon")]
SnapshotTooOld { txn_id: u64 },
#[error("database is busy")]
Busy,
#[error("database is busy (recovery in progress)")]
BusyRecovery,
#[error("database is busy (snapshot conflict on pages: {conflicting_pages})")]
BusySnapshot { conflicting_pages: String },
#[error(
"BEGIN CONCURRENT unavailable: fsqlite-shm not present (multi-writer MVCC requires shared memory coordination)"
)]
ConcurrentUnavailable,
#[error("type mismatch: expected {expected}, got {actual}")]
TypeMismatch { expected: String, actual: String },
#[error("integer overflow")]
IntegerOverflow,
#[error("{what} out of range: {value}")]
OutOfRange { what: String, value: String },
#[error("string or BLOB exceeds size limit")]
TooBig,
#[error("too many columns: {count} (max {max})")]
TooManyColumns { count: usize, max: usize },
#[error("SQL statement too long: {length} bytes (max {max})")]
SqlTooLong { length: usize, max: usize },
#[error("expression tree too deep (max {max})")]
ExpressionTooDeep { max: usize },
#[error("too many attached databases (max {max})")]
TooManyAttached { max: usize },
#[error("too many arguments to function {name}")]
TooManyArguments { name: String },
#[error("WAL file is corrupt: {detail}")]
WalCorrupt { detail: String },
#[error("WAL checkpoint failed: {detail}")]
CheckpointFailed { detail: String },
#[error("file locking failed: {detail}")]
LockFailed { detail: String },
#[error("unable to open database file: '{path}'")]
CannotOpen { path: PathBuf },
#[error("internal error: {0}")]
Internal(String),
#[error("unsupported operation")]
Unsupported,
#[error("not implemented: {0}")]
NotImplemented(String),
#[error("callback requested query abort")]
Abort,
#[error("authorization denied")]
AuthDenied,
#[error("out of memory")]
OutOfMemory,
#[error("{0}")]
FunctionError(String),
#[error("background worker failed: {0}")]
BackgroundWorkerFailed(String),
#[error("attempt to write a readonly database")]
ReadOnly,
#[error("interrupted")]
Interrupt,
#[error("VDBE execution error: {detail}")]
VdbeExecutionError { detail: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum ErrorCode {
Ok = 0,
Error = 1,
Internal = 2,
Perm = 3,
Abort = 4,
Busy = 5,
Locked = 6,
NoMem = 7,
ReadOnly = 8,
Interrupt = 9,
IoErr = 10,
Corrupt = 11,
NotFound = 12,
Full = 13,
CantOpen = 14,
Protocol = 15,
Empty = 16,
Schema = 17,
TooBig = 18,
Constraint = 19,
Mismatch = 20,
Misuse = 21,
NoLfs = 22,
Auth = 23,
Format = 24,
Range = 25,
NotADb = 26,
Notice = 27,
Warning = 28,
Row = 100,
Done = 101,
}
impl FrankenError {
#[allow(clippy::match_same_arms)]
pub const fn error_code(&self) -> ErrorCode {
match self {
Self::DatabaseNotFound { .. } | Self::CannotOpen { .. } => ErrorCode::CantOpen,
Self::DatabaseLocked { .. } | Self::MultiProcessContractViolation { .. } => {
ErrorCode::Busy
}
Self::DatabaseCorrupt { .. } | Self::WalCorrupt { .. } => ErrorCode::Corrupt,
Self::NotADatabase { .. } => ErrorCode::NotADb,
Self::DatabaseFull => ErrorCode::Full,
Self::SchemaChanged => ErrorCode::Schema,
Self::Io(_)
| Self::IoRead { .. }
| Self::IoWrite { .. }
| Self::ShortRead { .. }
| Self::CheckpointFailed { .. } => ErrorCode::IoErr,
Self::SyntaxError { .. }
| Self::ParseError { .. }
| Self::QueryReturnedNoRows
| Self::QueryReturnedMultipleRows
| Self::NoSuchTable { .. }
| Self::NoSuchColumn { .. }
| Self::NoSuchIndex { .. }
| Self::TableExists { .. }
| Self::IndexExists { .. }
| Self::AmbiguousColumn { .. }
| Self::NestedTransaction
| Self::VacuumWithinTransaction
| Self::NoActiveTransaction
| Self::TransactionRolledBack { .. }
| Self::TooManyColumns { .. }
| Self::SqlTooLong { .. }
| Self::ExpressionTooDeep { .. }
| Self::TooManyAttached { .. }
| Self::TooManyArguments { .. }
| Self::NotImplemented(_)
| Self::FunctionError(_)
| Self::BackgroundWorkerFailed(_)
| Self::ConcurrentUnavailable => ErrorCode::Error,
Self::UniqueViolation { .. }
| Self::NotNullViolation { .. }
| Self::CheckViolation { .. }
| Self::ForeignKeyViolation
| Self::PrimaryKeyViolation
| Self::DatatypeViolation { .. } => ErrorCode::Constraint,
Self::WriteConflict { .. }
| Self::SerializationFailure { .. }
| Self::Busy
| Self::BusyRecovery
| Self::BusySnapshot { .. }
| Self::SnapshotTooOld { .. }
| Self::LockFailed { .. } => ErrorCode::Busy,
Self::TypeMismatch { .. } => ErrorCode::Mismatch,
Self::IntegerOverflow | Self::OutOfRange { .. } => ErrorCode::Range,
Self::TooBig => ErrorCode::TooBig,
Self::Internal(_) => ErrorCode::Internal,
Self::Abort => ErrorCode::Abort,
Self::AuthDenied => ErrorCode::Auth,
Self::OutOfMemory => ErrorCode::NoMem,
Self::Unsupported => ErrorCode::NoLfs,
Self::ReadOnly => ErrorCode::ReadOnly,
Self::Interrupt => ErrorCode::Interrupt,
Self::VdbeExecutionError { .. } => ErrorCode::Error,
}
}
pub const fn is_user_recoverable(&self) -> bool {
matches!(
self,
Self::DatabaseNotFound { .. }
| Self::DatabaseLocked { .. }
| Self::Busy
| Self::BusyRecovery
| Self::BusySnapshot { .. }
| Self::Unsupported
| Self::SyntaxError { .. }
| Self::ParseError { .. }
| Self::QueryReturnedNoRows
| Self::QueryReturnedMultipleRows
| Self::NoSuchTable { .. }
| Self::NoSuchColumn { .. }
| Self::TypeMismatch { .. }
| Self::CannotOpen { .. }
)
}
pub const fn suggestion(&self) -> Option<&'static str> {
match self {
Self::DatabaseNotFound { .. } => Some("Check the file path or create a new database"),
Self::DatabaseLocked { .. } => {
Some("Close other connections or wait for the lock to be released")
}
Self::Busy | Self::BusyRecovery => Some("Retry the operation after a short delay"),
Self::BusySnapshot { .. } => {
Some("Retry the transaction; another writer committed to the same pages")
}
Self::WriteConflict { .. } | Self::SerializationFailure { .. } => {
Some("Retry the transaction; the conflict is transient")
}
Self::SnapshotTooOld { .. } => Some("Begin a new transaction to get a fresh snapshot"),
Self::DatabaseCorrupt { .. } => {
Some("Run PRAGMA integrity_check; restore from backup if needed")
}
Self::TooBig => Some("Reduce the size of the value being inserted"),
Self::NotImplemented(_) => Some("This feature is not yet available in FrankenSQLite"),
Self::ConcurrentUnavailable => Some(
"Use a filesystem that supports shared memory, or use BEGIN (serialized) instead",
),
Self::BackgroundWorkerFailed(_) => {
Some("Close and reopen the database; inspect the logged worker failure details")
}
Self::QueryReturnedNoRows => Some("Use query() when zero rows are acceptable"),
Self::QueryReturnedMultipleRows => {
Some("Use query() when multiple rows are acceptable, or tighten the query")
}
_ => None,
}
}
pub const fn is_transient(&self) -> bool {
matches!(
self,
Self::Busy
| Self::BusyRecovery
| Self::BusySnapshot { .. }
| Self::DatabaseLocked { .. }
| Self::WriteConflict { .. }
| Self::SerializationFailure { .. }
)
}
pub const fn exit_code(&self) -> i32 {
self.error_code() as i32
}
pub const fn extended_error_code(&self) -> i32 {
match self {
Self::Busy => 5, Self::BusyRecovery => 5 | (1 << 8), Self::BusySnapshot { .. } => 5 | (2 << 8), Self::DatatypeViolation { .. } => 3091, _ => self.error_code() as i32,
}
}
pub fn syntax(token: impl Into<String>) -> Self {
Self::SyntaxError {
token: token.into(),
}
}
pub fn parse(offset: usize, detail: impl Into<String>) -> Self {
Self::ParseError {
offset,
detail: detail.into(),
}
}
pub fn internal(msg: impl Into<String>) -> Self {
Self::Internal(msg.into())
}
pub fn not_implemented(feature: impl Into<String>) -> Self {
Self::NotImplemented(feature.into())
}
pub fn function_error(msg: impl Into<String>) -> Self {
Self::FunctionError(msg.into())
}
}
pub type Result<T> = std::result::Result<T, FrankenError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_display() {
let err = FrankenError::syntax("SELEC");
assert_eq!(err.to_string(), r#"near "SELEC": syntax error"#);
}
#[test]
fn error_display_corrupt() {
let err = FrankenError::DatabaseCorrupt {
detail: "invalid page header".to_owned(),
};
assert_eq!(
err.to_string(),
"database disk image is malformed: invalid page header"
);
}
#[test]
fn error_display_write_conflict() {
let err = FrankenError::WriteConflict {
page: 42,
holder: 7,
};
assert_eq!(
err.to_string(),
"write conflict on page 42: held by transaction 7"
);
}
#[test]
fn error_code_mapping() {
assert_eq!(FrankenError::syntax("x").error_code(), ErrorCode::Error);
assert_eq!(
FrankenError::QueryReturnedNoRows.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::QueryReturnedMultipleRows.error_code(),
ErrorCode::Error
);
assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
assert_eq!(FrankenError::Abort.error_code(), ErrorCode::Abort);
assert_eq!(
FrankenError::DatabaseCorrupt {
detail: String::new()
}
.error_code(),
ErrorCode::Corrupt
);
assert_eq!(FrankenError::DatabaseFull.error_code(), ErrorCode::Full);
assert_eq!(FrankenError::TooBig.error_code(), ErrorCode::TooBig);
assert_eq!(FrankenError::OutOfMemory.error_code(), ErrorCode::NoMem);
assert_eq!(FrankenError::AuthDenied.error_code(), ErrorCode::Auth);
}
#[test]
fn user_recoverable() {
assert!(FrankenError::Busy.is_user_recoverable());
assert!(FrankenError::QueryReturnedNoRows.is_user_recoverable());
assert!(FrankenError::QueryReturnedMultipleRows.is_user_recoverable());
assert!(FrankenError::syntax("x").is_user_recoverable());
assert!(!FrankenError::internal("bug").is_user_recoverable());
assert!(!FrankenError::DatabaseFull.is_user_recoverable());
}
#[test]
fn is_transient() {
assert!(FrankenError::Busy.is_transient());
assert!(FrankenError::BusyRecovery.is_transient());
assert!(FrankenError::WriteConflict { page: 1, holder: 1 }.is_transient());
assert!(!FrankenError::DatabaseFull.is_transient());
assert!(!FrankenError::syntax("x").is_transient());
}
#[test]
fn suggestions() {
assert!(FrankenError::Busy.suggestion().is_some());
assert!(FrankenError::not_implemented("CTE").suggestion().is_some());
assert!(
FrankenError::QueryReturnedMultipleRows
.suggestion()
.is_some()
);
assert!(FrankenError::DatabaseFull.suggestion().is_none());
}
#[test]
fn convenience_constructors() {
let expected_kw = "kw_where";
let err = FrankenError::syntax(expected_kw);
assert!(matches!(
err,
FrankenError::SyntaxError { token: got_kw } if got_kw == expected_kw
));
let err = FrankenError::parse(42, "unexpected token");
assert!(matches!(err, FrankenError::ParseError { offset: 42, .. }));
let err = FrankenError::internal("assertion failed");
let actual = match &err {
FrankenError::Internal(msg) => Some(msg.as_str()),
_ => None,
};
assert_eq!(actual, Some("assertion failed"));
let err = FrankenError::not_implemented("window functions");
let actual = match &err {
FrankenError::NotImplemented(msg) => Some(msg.as_str()),
_ => None,
};
assert_eq!(actual, Some("window functions"));
}
#[test]
fn io_error_from() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let err: FrankenError = io_err.into();
assert!(matches!(err, FrankenError::Io(_)));
assert_eq!(err.error_code(), ErrorCode::IoErr);
}
#[test]
fn error_code_values() {
assert_eq!(ErrorCode::Ok as i32, 0);
assert_eq!(ErrorCode::Error as i32, 1);
assert_eq!(ErrorCode::Busy as i32, 5);
assert_eq!(ErrorCode::Constraint as i32, 19);
assert_eq!(ErrorCode::Row as i32, 100);
assert_eq!(ErrorCode::Done as i32, 101);
}
#[test]
fn exit_code() {
assert_eq!(FrankenError::Busy.exit_code(), 5);
assert_eq!(FrankenError::internal("x").exit_code(), 2);
assert_eq!(FrankenError::syntax("x").exit_code(), 1);
}
#[test]
fn extended_error_codes() {
assert_eq!(FrankenError::Busy.extended_error_code(), 5);
assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
assert_eq!(FrankenError::BusyRecovery.extended_error_code(), 261);
assert_eq!(FrankenError::BusyRecovery.error_code(), ErrorCode::Busy);
let busy_snapshot = FrankenError::BusySnapshot {
conflicting_pages: "1, 2, 3".to_owned(),
};
assert_eq!(busy_snapshot.extended_error_code(), 517);
assert_eq!(busy_snapshot.error_code(), ErrorCode::Busy);
assert_eq!(FrankenError::Busy.error_code(), ErrorCode::Busy);
assert_eq!(FrankenError::BusyRecovery.error_code(), ErrorCode::Busy);
assert_eq!(busy_snapshot.error_code(), ErrorCode::Busy);
assert_ne!(
FrankenError::Busy.extended_error_code(),
busy_snapshot.extended_error_code()
);
assert_ne!(
FrankenError::BusyRecovery.extended_error_code(),
busy_snapshot.extended_error_code()
);
}
#[test]
fn constraint_errors() {
let err = FrankenError::UniqueViolation {
columns: "users.email".to_owned(),
};
assert_eq!(err.to_string(), "UNIQUE constraint failed: users.email");
assert_eq!(err.error_code(), ErrorCode::Constraint);
let err = FrankenError::NotNullViolation {
column: "name".to_owned(),
};
assert_eq!(err.to_string(), "NOT NULL constraint failed: name");
assert_eq!(
FrankenError::ForeignKeyViolation.to_string(),
"FOREIGN KEY constraint failed"
);
}
#[test]
fn mvcc_errors() {
let err = FrankenError::WriteConflict {
page: 5,
holder: 10,
};
assert!(err.is_transient());
assert_eq!(err.error_code(), ErrorCode::Busy);
let err = FrankenError::SerializationFailure { page: 5 };
assert!(err.is_transient());
let err = FrankenError::SnapshotTooOld { txn_id: 42 };
assert!(!err.is_transient());
assert!(err.suggestion().is_some());
}
#[test]
fn display_database_not_found() {
let err = FrankenError::DatabaseNotFound {
path: PathBuf::from("/tmp/test.db"),
};
assert_eq!(err.to_string(), "database not found: '/tmp/test.db'");
}
#[test]
fn display_database_locked() {
let err = FrankenError::DatabaseLocked {
path: PathBuf::from("/tmp/test.db"),
};
assert_eq!(err.to_string(), "database is locked: '/tmp/test.db'");
}
#[test]
fn display_not_a_database() {
let err = FrankenError::NotADatabase {
path: PathBuf::from("/tmp/random.bin"),
};
assert_eq!(err.to_string(), "file is not a database: '/tmp/random.bin'");
}
#[test]
fn display_database_full() {
assert_eq!(FrankenError::DatabaseFull.to_string(), "database is full");
}
#[test]
fn display_schema_changed() {
assert_eq!(
FrankenError::SchemaChanged.to_string(),
"database schema has changed"
);
}
#[test]
fn display_io_read_write() {
let err = FrankenError::IoRead { page: 17 };
assert_eq!(err.to_string(), "disk I/O error reading page 17");
let err = FrankenError::IoWrite { page: 42 };
assert_eq!(err.to_string(), "disk I/O error writing page 42");
}
#[test]
fn display_short_read() {
let err = FrankenError::ShortRead {
expected: 4096,
actual: 2048,
};
assert_eq!(err.to_string(), "short read: expected 4096 bytes, got 2048");
}
#[test]
fn display_no_such_table_column_index() {
assert_eq!(
FrankenError::NoSuchTable {
name: "users".to_owned()
}
.to_string(),
"no such table: users"
);
assert_eq!(
FrankenError::NoSuchColumn {
name: "email".to_owned()
}
.to_string(),
"no such column: email"
);
assert_eq!(
FrankenError::NoSuchIndex {
name: "idx_email".to_owned()
}
.to_string(),
"no such index: idx_email"
);
}
#[test]
fn display_already_exists() {
assert_eq!(
FrankenError::TableExists {
name: "t1".to_owned()
}
.to_string(),
"table t1 already exists"
);
assert_eq!(
FrankenError::IndexExists {
name: "i1".to_owned()
}
.to_string(),
"index i1 already exists"
);
}
#[test]
fn display_ambiguous_column() {
let err = FrankenError::AmbiguousColumn {
name: "id".to_owned(),
};
assert_eq!(err.to_string(), "ambiguous column name: id");
}
#[test]
fn display_transaction_errors() {
assert_eq!(
FrankenError::NestedTransaction.to_string(),
"cannot start a transaction within a transaction"
);
assert_eq!(
FrankenError::NoActiveTransaction.to_string(),
"cannot commit - no transaction is active"
);
assert_eq!(
FrankenError::VacuumWithinTransaction.to_string(),
"cannot VACUUM from within a transaction"
);
assert_eq!(
FrankenError::TransactionRolledBack {
reason: "constraint".to_owned()
}
.to_string(),
"transaction rolled back: constraint"
);
}
#[test]
fn display_serialization_failure() {
let err = FrankenError::SerializationFailure { page: 99 };
assert_eq!(
err.to_string(),
"serialization failure: page 99 was modified after snapshot"
);
}
#[test]
fn display_snapshot_too_old() {
let err = FrankenError::SnapshotTooOld { txn_id: 100 };
assert_eq!(
err.to_string(),
"snapshot too old: transaction 100 is below GC horizon"
);
}
#[test]
fn display_busy_variants() {
assert_eq!(FrankenError::Busy.to_string(), "database is busy");
assert_eq!(
FrankenError::BusyRecovery.to_string(),
"database is busy (recovery in progress)"
);
}
#[test]
fn display_concurrent_unavailable() {
let err = FrankenError::ConcurrentUnavailable;
assert!(err.to_string().contains("BEGIN CONCURRENT unavailable"));
}
#[test]
fn display_type_errors() {
let err = FrankenError::TypeMismatch {
expected: "INTEGER".to_owned(),
actual: "TEXT".to_owned(),
};
assert_eq!(err.to_string(), "type mismatch: expected INTEGER, got TEXT");
assert_eq!(
FrankenError::IntegerOverflow.to_string(),
"integer overflow"
);
let err = FrankenError::OutOfRange {
what: "page number".to_owned(),
value: "0".to_owned(),
};
assert_eq!(err.to_string(), "page number out of range: 0");
}
#[test]
fn display_limit_errors() {
assert_eq!(
FrankenError::TooBig.to_string(),
"string or BLOB exceeds size limit"
);
let err = FrankenError::TooManyColumns {
count: 2001,
max: 2000,
};
assert_eq!(err.to_string(), "too many columns: 2001 (max 2000)");
let err = FrankenError::SqlTooLong {
length: 2_000_000,
max: 1_000_000,
};
assert_eq!(
err.to_string(),
"SQL statement too long: 2000000 bytes (max 1000000)"
);
let err = FrankenError::ExpressionTooDeep { max: 1000 };
assert_eq!(err.to_string(), "expression tree too deep (max 1000)");
let err = FrankenError::TooManyAttached { max: 10 };
assert_eq!(err.to_string(), "too many attached databases (max 10)");
let err = FrankenError::TooManyArguments {
name: "my_func".to_owned(),
};
assert_eq!(err.to_string(), "too many arguments to function my_func");
}
#[test]
fn display_wal_errors() {
let err = FrankenError::WalCorrupt {
detail: "invalid checksum".to_owned(),
};
assert_eq!(err.to_string(), "WAL file is corrupt: invalid checksum");
let err = FrankenError::CheckpointFailed {
detail: "busy".to_owned(),
};
assert_eq!(err.to_string(), "WAL checkpoint failed: busy");
}
#[test]
fn display_vfs_errors() {
let err = FrankenError::LockFailed {
detail: "permission denied".to_owned(),
};
assert_eq!(err.to_string(), "file locking failed: permission denied");
let err = FrankenError::CannotOpen {
path: PathBuf::from("/readonly/test.db"),
};
assert_eq!(
err.to_string(),
"unable to open database file: '/readonly/test.db'"
);
}
#[test]
fn display_internal_errors() {
assert_eq!(
FrankenError::Internal("assertion failed".to_owned()).to_string(),
"internal error: assertion failed"
);
assert_eq!(
FrankenError::Unsupported.to_string(),
"unsupported operation"
);
assert_eq!(
FrankenError::NotImplemented("CTE".to_owned()).to_string(),
"not implemented: CTE"
);
assert_eq!(
FrankenError::Abort.to_string(),
"callback requested query abort"
);
assert_eq!(FrankenError::AuthDenied.to_string(), "authorization denied");
assert_eq!(FrankenError::OutOfMemory.to_string(), "out of memory");
assert_eq!(
FrankenError::ReadOnly.to_string(),
"attempt to write a readonly database"
);
}
#[test]
fn display_function_error() {
let err = FrankenError::FunctionError("domain error".to_owned());
assert_eq!(err.to_string(), "domain error");
}
#[test]
#[allow(clippy::too_many_lines)]
fn error_code_comprehensive_mapping() {
assert_eq!(
FrankenError::DatabaseNotFound {
path: PathBuf::new()
}
.error_code(),
ErrorCode::CantOpen
);
assert_eq!(
FrankenError::DatabaseLocked {
path: PathBuf::new()
}
.error_code(),
ErrorCode::Busy
);
assert_eq!(
FrankenError::NotADatabase {
path: PathBuf::new()
}
.error_code(),
ErrorCode::NotADb
);
assert_eq!(FrankenError::SchemaChanged.error_code(), ErrorCode::Schema);
assert_eq!(
FrankenError::IoRead { page: 1 }.error_code(),
ErrorCode::IoErr
);
assert_eq!(
FrankenError::IoWrite { page: 1 }.error_code(),
ErrorCode::IoErr
);
assert_eq!(
FrankenError::ShortRead {
expected: 1,
actual: 0
}
.error_code(),
ErrorCode::IoErr
);
assert_eq!(
FrankenError::NoSuchTable {
name: String::new()
}
.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::NoSuchColumn {
name: String::new()
}
.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::NoSuchIndex {
name: String::new()
}
.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::TableExists {
name: String::new()
}
.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::IndexExists {
name: String::new()
}
.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::AmbiguousColumn {
name: String::new()
}
.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::NestedTransaction.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::VacuumWithinTransaction.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::NoActiveTransaction.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::SerializationFailure { page: 1 }.error_code(),
ErrorCode::Busy
);
assert_eq!(
FrankenError::SnapshotTooOld { txn_id: 1 }.error_code(),
ErrorCode::Busy
);
assert_eq!(
FrankenError::LockFailed {
detail: String::new()
}
.error_code(),
ErrorCode::Busy
);
assert_eq!(
FrankenError::TypeMismatch {
expected: String::new(),
actual: String::new()
}
.error_code(),
ErrorCode::Mismatch
);
assert_eq!(FrankenError::IntegerOverflow.error_code(), ErrorCode::Range);
assert_eq!(
FrankenError::OutOfRange {
what: String::new(),
value: String::new()
}
.error_code(),
ErrorCode::Range
);
assert_eq!(
FrankenError::TooManyColumns { count: 1, max: 1 }.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::SqlTooLong { length: 1, max: 1 }.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::ExpressionTooDeep { max: 1 }.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::TooManyAttached { max: 1 }.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::TooManyArguments {
name: String::new()
}
.error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::WalCorrupt {
detail: String::new()
}
.error_code(),
ErrorCode::Corrupt
);
assert_eq!(
FrankenError::CheckpointFailed {
detail: String::new()
}
.error_code(),
ErrorCode::IoErr
);
assert_eq!(
FrankenError::CannotOpen {
path: PathBuf::new()
}
.error_code(),
ErrorCode::CantOpen
);
assert_eq!(
FrankenError::Internal(String::new()).error_code(),
ErrorCode::Internal
);
assert_eq!(FrankenError::Unsupported.error_code(), ErrorCode::NoLfs);
assert_eq!(FrankenError::Abort.error_code(), ErrorCode::Abort);
assert_eq!(FrankenError::ReadOnly.error_code(), ErrorCode::ReadOnly);
assert_eq!(
FrankenError::FunctionError(String::new()).error_code(),
ErrorCode::Error
);
assert_eq!(
FrankenError::ConcurrentUnavailable.error_code(),
ErrorCode::Error
);
}
#[test]
fn is_user_recoverable_comprehensive() {
assert!(
FrankenError::DatabaseNotFound {
path: PathBuf::new()
}
.is_user_recoverable()
);
assert!(
FrankenError::DatabaseLocked {
path: PathBuf::new()
}
.is_user_recoverable()
);
assert!(FrankenError::BusyRecovery.is_user_recoverable());
assert!(FrankenError::Unsupported.is_user_recoverable());
assert!(
FrankenError::ParseError {
offset: 0,
detail: String::new()
}
.is_user_recoverable()
);
assert!(
FrankenError::NoSuchTable {
name: String::new()
}
.is_user_recoverable()
);
assert!(
FrankenError::NoSuchColumn {
name: String::new()
}
.is_user_recoverable()
);
assert!(
FrankenError::TypeMismatch {
expected: String::new(),
actual: String::new()
}
.is_user_recoverable()
);
assert!(
FrankenError::CannotOpen {
path: PathBuf::new()
}
.is_user_recoverable()
);
assert!(
!FrankenError::NotADatabase {
path: PathBuf::new()
}
.is_user_recoverable()
);
assert!(!FrankenError::TooBig.is_user_recoverable());
assert!(!FrankenError::OutOfMemory.is_user_recoverable());
assert!(!FrankenError::WriteConflict { page: 1, holder: 1 }.is_user_recoverable());
assert!(
!FrankenError::UniqueViolation {
columns: String::new()
}
.is_user_recoverable()
);
assert!(!FrankenError::ReadOnly.is_user_recoverable());
assert!(!FrankenError::Abort.is_user_recoverable());
}
#[test]
fn is_transient_comprehensive() {
assert!(
FrankenError::DatabaseLocked {
path: PathBuf::new()
}
.is_transient()
);
assert!(FrankenError::SerializationFailure { page: 1 }.is_transient());
assert!(
!FrankenError::DatabaseCorrupt {
detail: String::new()
}
.is_transient()
);
assert!(
!FrankenError::NotADatabase {
path: PathBuf::new()
}
.is_transient()
);
assert!(!FrankenError::TooBig.is_transient());
assert!(!FrankenError::Internal(String::new()).is_transient());
assert!(!FrankenError::OutOfMemory.is_transient());
assert!(
!FrankenError::UniqueViolation {
columns: String::new()
}
.is_transient()
);
assert!(!FrankenError::ReadOnly.is_transient());
assert!(!FrankenError::ConcurrentUnavailable.is_transient());
}
#[test]
fn suggestion_comprehensive() {
assert!(
FrankenError::DatabaseNotFound {
path: PathBuf::new()
}
.suggestion()
.is_some()
);
assert!(
FrankenError::DatabaseLocked {
path: PathBuf::new()
}
.suggestion()
.is_some()
);
assert!(FrankenError::BusyRecovery.suggestion().is_some());
assert!(
FrankenError::WriteConflict { page: 1, holder: 1 }
.suggestion()
.is_some()
);
assert!(
FrankenError::SerializationFailure { page: 1 }
.suggestion()
.is_some()
);
assert!(
FrankenError::SnapshotTooOld { txn_id: 1 }
.suggestion()
.is_some()
);
assert!(
FrankenError::DatabaseCorrupt {
detail: String::new()
}
.suggestion()
.is_some()
);
assert!(FrankenError::TooBig.suggestion().is_some());
assert!(FrankenError::ConcurrentUnavailable.suggestion().is_some());
assert!(FrankenError::QueryReturnedNoRows.suggestion().is_some());
assert!(
FrankenError::QueryReturnedMultipleRows
.suggestion()
.is_some()
);
assert!(FrankenError::Abort.suggestion().is_none());
assert!(FrankenError::AuthDenied.suggestion().is_none());
assert!(FrankenError::OutOfMemory.suggestion().is_none());
assert!(FrankenError::Internal(String::new()).suggestion().is_none());
assert!(FrankenError::ReadOnly.suggestion().is_none());
}
#[test]
fn error_code_enum_repr_values() {
assert_eq!(ErrorCode::Internal as i32, 2);
assert_eq!(ErrorCode::Perm as i32, 3);
assert_eq!(ErrorCode::Abort as i32, 4);
assert_eq!(ErrorCode::Locked as i32, 6);
assert_eq!(ErrorCode::NoMem as i32, 7);
assert_eq!(ErrorCode::ReadOnly as i32, 8);
assert_eq!(ErrorCode::Interrupt as i32, 9);
assert_eq!(ErrorCode::IoErr as i32, 10);
assert_eq!(ErrorCode::Corrupt as i32, 11);
assert_eq!(ErrorCode::NotFound as i32, 12);
assert_eq!(ErrorCode::Full as i32, 13);
assert_eq!(ErrorCode::CantOpen as i32, 14);
assert_eq!(ErrorCode::Protocol as i32, 15);
assert_eq!(ErrorCode::Empty as i32, 16);
assert_eq!(ErrorCode::Schema as i32, 17);
assert_eq!(ErrorCode::TooBig as i32, 18);
assert_eq!(ErrorCode::Mismatch as i32, 20);
assert_eq!(ErrorCode::Misuse as i32, 21);
assert_eq!(ErrorCode::NoLfs as i32, 22);
assert_eq!(ErrorCode::Auth as i32, 23);
assert_eq!(ErrorCode::Format as i32, 24);
assert_eq!(ErrorCode::Range as i32, 25);
assert_eq!(ErrorCode::NotADb as i32, 26);
assert_eq!(ErrorCode::Notice as i32, 27);
assert_eq!(ErrorCode::Warning as i32, 28);
}
#[test]
fn error_code_clone_eq() {
let code = ErrorCode::Busy;
let cloned = code;
assert_eq!(code, cloned);
assert_eq!(code, ErrorCode::Busy);
assert_ne!(code, ErrorCode::Error);
}
#[test]
fn function_error_constructor() {
let err = FrankenError::function_error("division by zero");
let actual = match &err {
FrankenError::FunctionError(msg) => Some(msg.as_str()),
_ => None,
};
assert_eq!(actual, Some("division by zero"));
assert_eq!(err.error_code(), ErrorCode::Error);
}
#[test]
fn background_worker_failed_is_non_transient_generic_error() {
let err = FrankenError::BackgroundWorkerFailed("gc worker panic".to_owned());
assert_eq!(err.to_string(), "background worker failed: gc worker panic");
assert_eq!(err.error_code(), ErrorCode::Error);
assert!(!err.is_transient());
assert!(!err.is_user_recoverable());
assert!(err.suggestion().is_some());
}
#[test]
fn constraint_error_codes_all_variants() {
assert_eq!(
FrankenError::CheckViolation {
name: "ck1".to_owned()
}
.error_code(),
ErrorCode::Constraint
);
assert_eq!(
FrankenError::PrimaryKeyViolation.error_code(),
ErrorCode::Constraint
);
assert_eq!(
FrankenError::CheckViolation {
name: "ck1".to_owned()
}
.to_string(),
"CHECK constraint failed: ck1"
);
assert_eq!(
FrankenError::PrimaryKeyViolation.to_string(),
"PRIMARY KEY constraint failed"
);
let err = FrankenError::DatatypeViolation {
column: "t1.name".to_owned(),
column_type: "INTEGER".to_owned(),
actual: "TEXT".to_owned(),
};
assert_eq!(
err.to_string(),
"cannot store TEXT value in INTEGER column t1.name"
);
assert_eq!(err.error_code(), ErrorCode::Constraint);
assert_eq!(err.extended_error_code(), 3091);
}
#[test]
fn exit_code_matches_error_code() {
let cases: Vec<FrankenError> = vec![
FrankenError::DatabaseFull,
FrankenError::TooBig,
FrankenError::OutOfMemory,
FrankenError::AuthDenied,
FrankenError::Abort,
FrankenError::ReadOnly,
FrankenError::Unsupported,
];
for err in cases {
assert_eq!(err.exit_code(), err.error_code() as i32);
}
}
}