use thiserror::Error;
pub type Result<T> = std::result::Result<T, MongrelError>;
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum MongrelError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("database at {path} is locked: {message}")]
DatabaseLocked {
path: std::path::PathBuf,
message: String,
},
#[error("database still has {strong_handles} shared handles")]
DatabaseBusy { strong_handles: usize },
#[error("database handle belongs to process {owner_pid}, not post-fork process {current_pid}; open after exec")]
ForkedProcess { owner_pid: u32, current_pid: u32 },
#[error("serialization error: {0}")]
Serialization(#[from] bincode::Error),
#[error("corrupt wal record at offset {offset}: {reason}")]
CorruptWal { offset: u64, reason: String },
#[error("torn wal write detected at offset {offset}")]
TornWrite { offset: u64 },
#[error("checksum mismatch for {context}: expected {expected}, got {actual}")]
ChecksumMismatch {
expected: u64,
actual: u64,
context: String,
},
#[error("magic mismatch in {what}: expected {expected:?}, got {got:?}")]
MagicMismatch {
what: &'static str,
expected: [u8; 8],
got: [u8; 8],
},
#[error("unsupported {component} storage version {found}: this build supports version {supported} only; recreate the database or export data using the engine version that created it")]
UnsupportedStorageVersion {
component: &'static str,
found: u16,
supported: u16,
},
#[error("schema error: {0}")]
Schema(String),
#[error("column not found: {0}")]
ColumnNotFound(String),
#[error("encryption is required for this table but the `encryption` feature is disabled")]
EncryptionDisabled,
#[error("encryption error: {0}")]
Encryption(String),
#[error("decryption error: {0}")]
Decryption(String),
#[error("OS CSPRNG unavailable: {0}")]
EntropyUnavailable(String),
#[error("not found: {0}")]
NotFound(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("table is full: {0}")]
Full(String),
#[error("transaction conflict: {0}")]
Conflict(String),
#[error(
"deadlock: transaction {victim} was chosen as the deadlock victim (wait-for cycle {cycle}); retry the whole transaction"
)]
Deadlock { victim: u64, cycle: String },
#[error("serialization failure: {message}")]
SerializationFailure { message: String },
#[error("trigger validation failed: {0}")]
TriggerValidation(String),
#[error("read-only replica: writes must be applied by ReplicationFollower")]
ReadOnlyReplica,
#[error("authentication required: this database has require_auth enabled; reopen with open_with_credentials / open_encrypted_with_credentials")]
AuthRequired,
#[error("authentication not required: this database does not have require_auth enabled; use the plain open/create constructors")]
AuthNotRequired,
#[error("invalid credentials for user {username:?}")]
InvalidCredentials { username: String },
#[error("permission denied: principal {principal:?} lacks {required}")]
PermissionDenied {
required: crate::auth::Permission,
principal: String,
},
#[error("execution deadline exceeded")]
DeadlineExceeded,
#[error("AI query work budget exceeded")]
WorkBudgetExceeded,
#[error(
"execution resource limit exceeded for {resource}: requested {requested}, limit {limit}"
)]
ResourceLimitExceeded {
resource: &'static str,
requested: usize,
limit: usize,
},
#[error("execution cancelled")]
Cancelled,
#[error("commit {epoch} is durable: {message}")]
DurableCommit { epoch: u64, message: String },
#[error("commit outcome at epoch {epoch} is unknown: {message}")]
CommitOutcomeUnknown { epoch: u64, message: String },
#[error("cursor stale: {0}")]
CursorStale(String),
#[error("cursor expired")]
CursorExpired,
#[error("{0}")]
Other(String),
}
impl MongrelError {
pub fn category(&self) -> mongreldb_types::errors::ErrorCategory {
use mongreldb_types::errors::ErrorCategory;
match self {
MongrelError::Io(_) => ErrorCategory::ReplicaUnavailable,
MongrelError::DatabaseLocked { .. } => ErrorCategory::ResourceExhausted,
MongrelError::DatabaseBusy { .. } => ErrorCategory::ResourceExhausted,
MongrelError::ForkedProcess { .. } => ErrorCategory::PermissionDenied,
MongrelError::Serialization(_) => ErrorCategory::ClusterVersionMismatch,
MongrelError::CorruptWal { .. }
| MongrelError::TornWrite { .. }
| MongrelError::ChecksumMismatch { .. }
| MongrelError::MagicMismatch { .. } => ErrorCategory::ReplicaUnavailable,
MongrelError::UnsupportedStorageVersion { .. } => ErrorCategory::ClusterVersionMismatch,
MongrelError::Schema(_) | MongrelError::ColumnNotFound(_) => {
ErrorCategory::SchemaVersionMismatch
}
MongrelError::EncryptionDisabled => ErrorCategory::ClusterVersionMismatch,
MongrelError::Encryption(_) | MongrelError::Decryption(_) => {
ErrorCategory::Unauthenticated
}
MongrelError::EntropyUnavailable(_) => ErrorCategory::ResourceExhausted,
MongrelError::NotFound(_) => ErrorCategory::StaleMetadata,
MongrelError::InvalidArgument(_) => ErrorCategory::ClusterVersionMismatch,
MongrelError::Full(_) => ErrorCategory::ResourceExhausted,
MongrelError::Conflict(_) => ErrorCategory::TransactionConflict,
MongrelError::Deadlock { .. } => ErrorCategory::Deadlock,
MongrelError::SerializationFailure { .. } => ErrorCategory::SerializationFailure,
MongrelError::TriggerValidation(_) => ErrorCategory::SchemaVersionMismatch,
MongrelError::ReadOnlyReplica => ErrorCategory::NotLeader,
MongrelError::AuthRequired => ErrorCategory::Unauthenticated,
MongrelError::AuthNotRequired => ErrorCategory::ClusterVersionMismatch,
MongrelError::InvalidCredentials { .. } => ErrorCategory::Unauthenticated,
MongrelError::PermissionDenied { .. } => ErrorCategory::PermissionDenied,
MongrelError::DeadlineExceeded => ErrorCategory::DeadlineExceeded,
MongrelError::WorkBudgetExceeded | MongrelError::ResourceLimitExceeded { .. } => {
ErrorCategory::ResourceExhausted
}
MongrelError::Cancelled => ErrorCategory::Cancelled,
MongrelError::DurableCommit { .. } | MongrelError::CommitOutcomeUnknown { .. } => {
ErrorCategory::CommitOutcomeUnknown
}
MongrelError::CursorStale(_) | MongrelError::CursorExpired => {
ErrorCategory::StaleMetadata
}
MongrelError::Other(_) => ErrorCategory::ReplicaUnavailable,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use mongreldb_types::errors::ErrorCategory;
fn io_error() -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied")
}
fn bincode_error() -> bincode::Error {
bincode::ErrorKind::Custom("codec".to_string()).into()
}
#[test]
fn category_mapping_is_total_and_matches_expectations() {
let cases: Vec<(MongrelError, ErrorCategory)> = vec![
(
MongrelError::Io(io_error()),
ErrorCategory::ReplicaUnavailable,
),
(
MongrelError::DatabaseLocked {
path: "db.mdb".into(),
message: "held".into(),
},
ErrorCategory::ResourceExhausted,
),
(
MongrelError::DatabaseBusy { strong_handles: 2 },
ErrorCategory::ResourceExhausted,
),
(
MongrelError::ForkedProcess {
owner_pid: 1,
current_pid: 2,
},
ErrorCategory::PermissionDenied,
),
(
MongrelError::Serialization(bincode_error()),
ErrorCategory::ClusterVersionMismatch,
),
(
MongrelError::CorruptWal {
offset: 8,
reason: "bad".into(),
},
ErrorCategory::ReplicaUnavailable,
),
(
MongrelError::TornWrite { offset: 16 },
ErrorCategory::ReplicaUnavailable,
),
(
MongrelError::ChecksumMismatch {
expected: 1,
actual: 2,
context: "page".into(),
},
ErrorCategory::ReplicaUnavailable,
),
(
MongrelError::MagicMismatch {
what: "wal",
expected: [1; 8],
got: [2; 8],
},
ErrorCategory::ReplicaUnavailable,
),
(
MongrelError::UnsupportedStorageVersion {
component: "wal",
found: 2,
supported: 1,
},
ErrorCategory::ClusterVersionMismatch,
),
(
MongrelError::Schema("bad ddl".into()),
ErrorCategory::SchemaVersionMismatch,
),
(
MongrelError::ColumnNotFound("c".into()),
ErrorCategory::SchemaVersionMismatch,
),
(
MongrelError::EncryptionDisabled,
ErrorCategory::ClusterVersionMismatch,
),
(
MongrelError::Encryption("seal".into()),
ErrorCategory::Unauthenticated,
),
(
MongrelError::Decryption("open".into()),
ErrorCategory::Unauthenticated,
),
(
MongrelError::EntropyUnavailable("rng".into()),
ErrorCategory::ResourceExhausted,
),
(
MongrelError::NotFound("row".into()),
ErrorCategory::StaleMetadata,
),
(
MongrelError::InvalidArgument("arg".into()),
ErrorCategory::ClusterVersionMismatch,
),
(
MongrelError::Full("table".into()),
ErrorCategory::ResourceExhausted,
),
(
MongrelError::Conflict("rw".into()),
ErrorCategory::TransactionConflict,
),
(
MongrelError::Deadlock {
victim: 3,
cycle: "3 → 1 → 3".into(),
},
ErrorCategory::Deadlock,
),
(
MongrelError::SerializationFailure {
message: "a concurrent commit invalidated this transaction's reads".into(),
},
ErrorCategory::SerializationFailure,
),
(
MongrelError::TriggerValidation("ck".into()),
ErrorCategory::SchemaVersionMismatch,
),
(MongrelError::ReadOnlyReplica, ErrorCategory::NotLeader),
(MongrelError::AuthRequired, ErrorCategory::Unauthenticated),
(
MongrelError::AuthNotRequired,
ErrorCategory::ClusterVersionMismatch,
),
(
MongrelError::InvalidCredentials {
username: "alice".into(),
},
ErrorCategory::Unauthenticated,
),
(
MongrelError::PermissionDenied {
required: crate::auth::Permission::Admin,
principal: "bob".into(),
},
ErrorCategory::PermissionDenied,
),
(
MongrelError::DeadlineExceeded,
ErrorCategory::DeadlineExceeded,
),
(
MongrelError::WorkBudgetExceeded,
ErrorCategory::ResourceExhausted,
),
(
MongrelError::ResourceLimitExceeded {
resource: "rows",
requested: 10,
limit: 5,
},
ErrorCategory::ResourceExhausted,
),
(MongrelError::Cancelled, ErrorCategory::Cancelled),
(
MongrelError::DurableCommit {
epoch: 9,
message: "callback".into(),
},
ErrorCategory::CommitOutcomeUnknown,
),
(
MongrelError::CommitOutcomeUnknown {
epoch: 10,
message: "lost".into(),
},
ErrorCategory::CommitOutcomeUnknown,
),
(
MongrelError::CursorStale("gen".into()),
ErrorCategory::StaleMetadata,
),
(MongrelError::CursorExpired, ErrorCategory::StaleMetadata),
(
MongrelError::Other("misc".into()),
ErrorCategory::ReplicaUnavailable,
),
];
assert_eq!(
cases.len(),
37,
"every MongrelError variant must appear in the mapping table"
);
for (error, expected) in cases {
assert_eq!(error.category(), expected, "wrong category for {error}");
}
}
#[test]
fn prescribed_fnd_007_mappings_hold() {
assert_eq!(
MongrelError::Conflict("x".into()).category(),
ErrorCategory::TransactionConflict
);
assert_eq!(MongrelError::Cancelled.category(), ErrorCategory::Cancelled);
assert_eq!(
MongrelError::CommitOutcomeUnknown {
epoch: 1,
message: "m".into(),
}
.category(),
ErrorCategory::CommitOutcomeUnknown
);
assert_eq!(
MongrelError::DatabaseLocked {
path: "p".into(),
message: "m".into(),
}
.category(),
ErrorCategory::ResourceExhausted
);
assert_eq!(
MongrelError::ReadOnlyReplica.category(),
ErrorCategory::NotLeader
);
assert_eq!(
MongrelError::Deadlock {
victim: 2,
cycle: "2 → 1 → 2".into(),
}
.category(),
ErrorCategory::Deadlock
);
assert_eq!(
MongrelError::SerializationFailure {
message: "m".into(),
}
.category(),
ErrorCategory::SerializationFailure
);
}
#[test]
fn deadlock_display_names_victim_and_cycle() {
let error = MongrelError::Deadlock {
victim: 7,
cycle: "7 → 4 → 7".into(),
};
let display = error.to_string();
assert!(
display.starts_with("deadlock: transaction 7"),
"display names the victim: {display}"
);
assert!(
display.contains("7 → 4 → 7"),
"display carries the wait-for cycle: {display}"
);
}
#[test]
fn serialization_failure_display_keeps_the_stable_marker() {
let error = MongrelError::SerializationFailure {
message: "a concurrent commit invalidated this transaction's reads".into(),
};
assert!(
error.to_string().starts_with("serialization failure: "),
"display keeps the marker prefix: {error}"
);
}
}