1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, MongrelError>;
4
5#[non_exhaustive]
6#[derive(Debug, Error)]
7pub enum MongrelError {
8 #[error("io error: {0}")]
9 Io(#[from] std::io::Error),
10 #[error("database at {path} is locked: {message}")]
11 DatabaseLocked {
12 path: std::path::PathBuf,
13 message: String,
14 },
15 #[error("database still has {strong_handles} shared handles")]
16 DatabaseBusy { strong_handles: usize },
17 #[error("database handle belongs to process {owner_pid}, not post-fork process {current_pid}; open after exec")]
18 ForkedProcess { owner_pid: u32, current_pid: u32 },
19 #[error("serialization error: {0}")]
20 Serialization(#[from] bincode::Error),
21 #[error("corrupt wal record at offset {offset}: {reason}")]
22 CorruptWal { offset: u64, reason: String },
23 #[error("torn wal write detected at offset {offset}")]
24 TornWrite { offset: u64 },
25 #[error("checksum mismatch for {context}: expected {expected}, got {actual}")]
26 ChecksumMismatch {
27 expected: u64,
28 actual: u64,
29 context: String,
30 },
31 #[error("magic mismatch in {what}: expected {expected:?}, got {got:?}")]
32 MagicMismatch {
33 what: &'static str,
34 expected: [u8; 8],
35 got: [u8; 8],
36 },
37 #[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")]
38 UnsupportedStorageVersion {
39 component: &'static str,
40 found: u16,
41 supported: u16,
42 },
43 #[error("schema error: {0}")]
44 Schema(String),
45 #[error("column not found: {0}")]
46 ColumnNotFound(String),
47 #[error("encryption is required for this table but the `encryption` feature is disabled")]
48 EncryptionDisabled,
49 #[error("encryption error: {0}")]
50 Encryption(String),
51 #[error("decryption error: {0}")]
52 Decryption(String),
53 #[error("OS CSPRNG unavailable: {0}")]
54 EntropyUnavailable(String),
55 #[error("not found: {0}")]
56 NotFound(String),
57 #[error("invalid argument: {0}")]
58 InvalidArgument(String),
59 #[error("table is full: {0}")]
60 Full(String),
61 #[error("transaction conflict: {0}")]
62 Conflict(String),
63 #[error(
64 "deadlock: transaction {victim} was chosen as the deadlock victim (wait-for cycle {cycle}); retry the whole transaction"
65 )]
66 Deadlock { victim: u64, cycle: String },
67 #[error("serialization failure: {message}")]
68 SerializationFailure { message: String },
69 #[error("trigger validation failed: {0}")]
70 TriggerValidation(String),
71 #[error("read-only replica: writes must be applied by ReplicationFollower")]
72 ReadOnlyReplica,
73 #[error("authentication required: this database has require_auth enabled; reopen with open_with_credentials / open_encrypted_with_credentials")]
74 AuthRequired,
75 #[error("authentication not required: this database does not have require_auth enabled; use the plain open/create constructors")]
76 AuthNotRequired,
77 #[error("invalid credentials for user {username:?}")]
78 InvalidCredentials { username: String },
79 #[error("permission denied: principal {principal:?} lacks {required}")]
80 PermissionDenied {
81 required: crate::auth::Permission,
82 principal: String,
83 },
84 #[error("execution deadline exceeded")]
85 DeadlineExceeded,
86 #[error("AI query work budget exceeded")]
87 WorkBudgetExceeded,
88 #[error(
89 "execution resource limit exceeded for {resource}: requested {requested}, limit {limit}"
90 )]
91 ResourceLimitExceeded {
92 resource: &'static str,
93 requested: usize,
94 limit: usize,
95 },
96 #[error("execution cancelled")]
97 Cancelled,
98 #[error("commit {epoch} is durable: {message}")]
99 DurableCommit { epoch: u64, message: String },
100 #[error("commit outcome at epoch {epoch} is unknown: {message}")]
101 CommitOutcomeUnknown { epoch: u64, message: String },
102 #[error("cursor stale: {0}")]
103 CursorStale(String),
104 #[error("cursor expired")]
105 CursorExpired,
106 #[error("{0}")]
107 Other(String),
108}
109
110impl MongrelError {
111 pub fn category(&self) -> mongreldb_types::errors::ErrorCategory {
120 use mongreldb_types::errors::ErrorCategory;
121 match self {
122 MongrelError::Io(_) => ErrorCategory::ReplicaUnavailable,
125 MongrelError::DatabaseLocked { .. } => ErrorCategory::ResourceExhausted,
127 MongrelError::DatabaseBusy { .. } => ErrorCategory::ResourceExhausted,
129 MongrelError::ForkedProcess { .. } => ErrorCategory::PermissionDenied,
133 MongrelError::Serialization(_) => ErrorCategory::ClusterVersionMismatch,
137 MongrelError::CorruptWal { .. }
140 | MongrelError::TornWrite { .. }
141 | MongrelError::ChecksumMismatch { .. }
142 | MongrelError::MagicMismatch { .. } => ErrorCategory::ReplicaUnavailable,
143 MongrelError::UnsupportedStorageVersion { .. } => ErrorCategory::ClusterVersionMismatch,
145 MongrelError::Schema(_) | MongrelError::ColumnNotFound(_) => {
150 ErrorCategory::SchemaVersionMismatch
151 }
152 MongrelError::EncryptionDisabled => ErrorCategory::ClusterVersionMismatch,
155 MongrelError::Encryption(_) | MongrelError::Decryption(_) => {
158 ErrorCategory::Unauthenticated
159 }
160 MongrelError::EntropyUnavailable(_) => ErrorCategory::ResourceExhausted,
163 MongrelError::NotFound(_) => ErrorCategory::StaleMetadata,
167 MongrelError::InvalidArgument(_) => ErrorCategory::ClusterVersionMismatch,
171 MongrelError::Full(_) => ErrorCategory::ResourceExhausted,
172 MongrelError::Conflict(_) => ErrorCategory::TransactionConflict,
173 MongrelError::Deadlock { .. } => ErrorCategory::Deadlock,
179 MongrelError::SerializationFailure { .. } => ErrorCategory::SerializationFailure,
180 MongrelError::TriggerValidation(_) => ErrorCategory::SchemaVersionMismatch,
184 MongrelError::ReadOnlyReplica => ErrorCategory::NotLeader,
187 MongrelError::AuthRequired => ErrorCategory::Unauthenticated,
188 MongrelError::AuthNotRequired => ErrorCategory::ClusterVersionMismatch,
192 MongrelError::InvalidCredentials { .. } => ErrorCategory::Unauthenticated,
193 MongrelError::PermissionDenied { .. } => ErrorCategory::PermissionDenied,
194 MongrelError::DeadlineExceeded => ErrorCategory::DeadlineExceeded,
195 MongrelError::WorkBudgetExceeded | MongrelError::ResourceLimitExceeded { .. } => {
196 ErrorCategory::ResourceExhausted
197 }
198 MongrelError::Cancelled => ErrorCategory::Cancelled,
199 MongrelError::DurableCommit { .. } | MongrelError::CommitOutcomeUnknown { .. } => {
206 ErrorCategory::CommitOutcomeUnknown
207 }
208 MongrelError::CursorStale(_) | MongrelError::CursorExpired => {
211 ErrorCategory::StaleMetadata
212 }
213 MongrelError::Other(_) => ErrorCategory::ReplicaUnavailable,
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use mongreldb_types::errors::ErrorCategory;
225
226 fn io_error() -> std::io::Error {
227 std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied")
228 }
229
230 fn bincode_error() -> bincode::Error {
231 bincode::ErrorKind::Custom("codec".to_string()).into()
232 }
233
234 #[test]
235 fn category_mapping_is_total_and_matches_expectations() {
236 let cases: Vec<(MongrelError, ErrorCategory)> = vec![
237 (
238 MongrelError::Io(io_error()),
239 ErrorCategory::ReplicaUnavailable,
240 ),
241 (
242 MongrelError::DatabaseLocked {
243 path: "db.mdb".into(),
244 message: "held".into(),
245 },
246 ErrorCategory::ResourceExhausted,
247 ),
248 (
249 MongrelError::DatabaseBusy { strong_handles: 2 },
250 ErrorCategory::ResourceExhausted,
251 ),
252 (
253 MongrelError::ForkedProcess {
254 owner_pid: 1,
255 current_pid: 2,
256 },
257 ErrorCategory::PermissionDenied,
258 ),
259 (
260 MongrelError::Serialization(bincode_error()),
261 ErrorCategory::ClusterVersionMismatch,
262 ),
263 (
264 MongrelError::CorruptWal {
265 offset: 8,
266 reason: "bad".into(),
267 },
268 ErrorCategory::ReplicaUnavailable,
269 ),
270 (
271 MongrelError::TornWrite { offset: 16 },
272 ErrorCategory::ReplicaUnavailable,
273 ),
274 (
275 MongrelError::ChecksumMismatch {
276 expected: 1,
277 actual: 2,
278 context: "page".into(),
279 },
280 ErrorCategory::ReplicaUnavailable,
281 ),
282 (
283 MongrelError::MagicMismatch {
284 what: "wal",
285 expected: [1; 8],
286 got: [2; 8],
287 },
288 ErrorCategory::ReplicaUnavailable,
289 ),
290 (
291 MongrelError::UnsupportedStorageVersion {
292 component: "wal",
293 found: 2,
294 supported: 1,
295 },
296 ErrorCategory::ClusterVersionMismatch,
297 ),
298 (
299 MongrelError::Schema("bad ddl".into()),
300 ErrorCategory::SchemaVersionMismatch,
301 ),
302 (
303 MongrelError::ColumnNotFound("c".into()),
304 ErrorCategory::SchemaVersionMismatch,
305 ),
306 (
307 MongrelError::EncryptionDisabled,
308 ErrorCategory::ClusterVersionMismatch,
309 ),
310 (
311 MongrelError::Encryption("seal".into()),
312 ErrorCategory::Unauthenticated,
313 ),
314 (
315 MongrelError::Decryption("open".into()),
316 ErrorCategory::Unauthenticated,
317 ),
318 (
319 MongrelError::EntropyUnavailable("rng".into()),
320 ErrorCategory::ResourceExhausted,
321 ),
322 (
323 MongrelError::NotFound("row".into()),
324 ErrorCategory::StaleMetadata,
325 ),
326 (
327 MongrelError::InvalidArgument("arg".into()),
328 ErrorCategory::ClusterVersionMismatch,
329 ),
330 (
331 MongrelError::Full("table".into()),
332 ErrorCategory::ResourceExhausted,
333 ),
334 (
335 MongrelError::Conflict("rw".into()),
336 ErrorCategory::TransactionConflict,
337 ),
338 (
339 MongrelError::Deadlock {
340 victim: 3,
341 cycle: "3 → 1 → 3".into(),
342 },
343 ErrorCategory::Deadlock,
344 ),
345 (
346 MongrelError::SerializationFailure {
347 message: "a concurrent commit invalidated this transaction's reads".into(),
348 },
349 ErrorCategory::SerializationFailure,
350 ),
351 (
352 MongrelError::TriggerValidation("ck".into()),
353 ErrorCategory::SchemaVersionMismatch,
354 ),
355 (MongrelError::ReadOnlyReplica, ErrorCategory::NotLeader),
356 (MongrelError::AuthRequired, ErrorCategory::Unauthenticated),
357 (
358 MongrelError::AuthNotRequired,
359 ErrorCategory::ClusterVersionMismatch,
360 ),
361 (
362 MongrelError::InvalidCredentials {
363 username: "alice".into(),
364 },
365 ErrorCategory::Unauthenticated,
366 ),
367 (
368 MongrelError::PermissionDenied {
369 required: crate::auth::Permission::Admin,
370 principal: "bob".into(),
371 },
372 ErrorCategory::PermissionDenied,
373 ),
374 (
375 MongrelError::DeadlineExceeded,
376 ErrorCategory::DeadlineExceeded,
377 ),
378 (
379 MongrelError::WorkBudgetExceeded,
380 ErrorCategory::ResourceExhausted,
381 ),
382 (
383 MongrelError::ResourceLimitExceeded {
384 resource: "rows",
385 requested: 10,
386 limit: 5,
387 },
388 ErrorCategory::ResourceExhausted,
389 ),
390 (MongrelError::Cancelled, ErrorCategory::Cancelled),
391 (
392 MongrelError::DurableCommit {
393 epoch: 9,
394 message: "callback".into(),
395 },
396 ErrorCategory::CommitOutcomeUnknown,
397 ),
398 (
399 MongrelError::CommitOutcomeUnknown {
400 epoch: 10,
401 message: "lost".into(),
402 },
403 ErrorCategory::CommitOutcomeUnknown,
404 ),
405 (
406 MongrelError::CursorStale("gen".into()),
407 ErrorCategory::StaleMetadata,
408 ),
409 (MongrelError::CursorExpired, ErrorCategory::StaleMetadata),
410 (
411 MongrelError::Other("misc".into()),
412 ErrorCategory::ReplicaUnavailable,
413 ),
414 ];
415 assert_eq!(
416 cases.len(),
417 37,
418 "every MongrelError variant must appear in the mapping table"
419 );
420 for (error, expected) in cases {
421 assert_eq!(error.category(), expected, "wrong category for {error}");
422 }
423 }
424
425 #[test]
426 fn prescribed_fnd_007_mappings_hold() {
427 assert_eq!(
428 MongrelError::Conflict("x".into()).category(),
429 ErrorCategory::TransactionConflict
430 );
431 assert_eq!(MongrelError::Cancelled.category(), ErrorCategory::Cancelled);
432 assert_eq!(
433 MongrelError::CommitOutcomeUnknown {
434 epoch: 1,
435 message: "m".into(),
436 }
437 .category(),
438 ErrorCategory::CommitOutcomeUnknown
439 );
440 assert_eq!(
441 MongrelError::DatabaseLocked {
442 path: "p".into(),
443 message: "m".into(),
444 }
445 .category(),
446 ErrorCategory::ResourceExhausted
447 );
448 assert_eq!(
449 MongrelError::ReadOnlyReplica.category(),
450 ErrorCategory::NotLeader
451 );
452 assert_eq!(
453 MongrelError::Deadlock {
454 victim: 2,
455 cycle: "2 → 1 → 2".into(),
456 }
457 .category(),
458 ErrorCategory::Deadlock
459 );
460 assert_eq!(
461 MongrelError::SerializationFailure {
462 message: "m".into(),
463 }
464 .category(),
465 ErrorCategory::SerializationFailure
466 );
467 }
468
469 #[test]
470 fn deadlock_display_names_victim_and_cycle() {
471 let error = MongrelError::Deadlock {
472 victim: 7,
473 cycle: "7 → 4 → 7".into(),
474 };
475 let display = error.to_string();
476 assert!(
477 display.starts_with("deadlock: transaction 7"),
478 "display names the victim: {display}"
479 );
480 assert!(
481 display.contains("7 → 4 → 7"),
482 "display carries the wait-for cycle: {display}"
483 );
484 }
485
486 #[test]
487 fn serialization_failure_display_keeps_the_stable_marker() {
488 let error = MongrelError::SerializationFailure {
492 message: "a concurrent commit invalidated this transaction's reads".into(),
493 };
494 assert!(
495 error.to_string().starts_with("serialization failure: "),
496 "display keeps the marker prefix: {error}"
497 );
498 }
499}