1use thiserror::Error;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
37#[non_exhaustive]
38pub enum EnvironmentFailureReason {
39 LogChecksum,
43
44 LogWrite,
46
47 LogFileNotFound,
50
51 LogIntegrity,
54
55 BtreeCorruption,
59
60 UnexpectedState,
64
65 UnexpectedStateFatal,
68
69 UnexpectedException,
72
73 UnexpectedExceptionFatal,
76
77 DiskLimit,
81
82 LatchTimeout,
84
85 ThreadInterrupted,
89
90 MasterToReplicaTransition,
94
95 ReplicaFencing,
98
99 HandshakeError,
102
103 ProtocolVersionMismatch,
106
107 UncaughtException,
110
111 ForcedShutdown,
114
115 RecoveryFailure,
126
127 Other(String),
129}
130
131impl EnvironmentFailureReason {
132 pub fn invalidates_environment(&self) -> bool {
139 matches!(
140 self,
141 EnvironmentFailureReason::LogChecksum
142 | EnvironmentFailureReason::LogWrite
143 | EnvironmentFailureReason::LogIntegrity
144 | EnvironmentFailureReason::BtreeCorruption
145 | EnvironmentFailureReason::UnexpectedStateFatal
146 | EnvironmentFailureReason::UnexpectedExceptionFatal
147 | EnvironmentFailureReason::RecoveryFailure
148 | EnvironmentFailureReason::DiskLimit
149 | EnvironmentFailureReason::LatchTimeout
150 | EnvironmentFailureReason::ReplicaFencing
151 | EnvironmentFailureReason::ForcedShutdown
152 )
153 }
154
155 pub fn is_corrupted(&self) -> bool {
160 matches!(
161 self,
162 EnvironmentFailureReason::LogChecksum
163 | EnvironmentFailureReason::BtreeCorruption
164 )
165 }
166}
167
168impl std::fmt::Display for EnvironmentFailureReason {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 EnvironmentFailureReason::LogChecksum => write!(f, "LOG_CHECKSUM"),
172 EnvironmentFailureReason::LogWrite => write!(f, "LOG_WRITE"),
173 EnvironmentFailureReason::LogFileNotFound => {
174 write!(f, "LOG_FILE_NOT_FOUND")
175 }
176 EnvironmentFailureReason::LogIntegrity => {
177 write!(f, "LOG_INTEGRITY")
178 }
179 EnvironmentFailureReason::BtreeCorruption => {
180 write!(f, "BTREE_CORRUPTION")
181 }
182 EnvironmentFailureReason::UnexpectedState => {
183 write!(f, "UNEXPECTED_STATE")
184 }
185 EnvironmentFailureReason::UnexpectedStateFatal => {
186 write!(f, "UNEXPECTED_STATE_FATAL")
187 }
188 EnvironmentFailureReason::UnexpectedException => {
189 write!(f, "UNEXPECTED_EXCEPTION")
190 }
191 EnvironmentFailureReason::UnexpectedExceptionFatal => {
192 write!(f, "UNEXPECTED_EXCEPTION_FATAL")
193 }
194 EnvironmentFailureReason::DiskLimit => write!(f, "DISK_LIMIT"),
195 EnvironmentFailureReason::LatchTimeout => {
196 write!(f, "LATCH_TIMEOUT")
197 }
198 EnvironmentFailureReason::ThreadInterrupted => {
199 write!(f, "THREAD_INTERRUPTED")
200 }
201 EnvironmentFailureReason::MasterToReplicaTransition => {
202 write!(f, "MASTER_TO_REPLICA_TRANSITION")
203 }
204 EnvironmentFailureReason::ReplicaFencing => {
205 write!(f, "REPLICA_FENCING")
206 }
207 EnvironmentFailureReason::HandshakeError => {
208 write!(f, "HANDSHAKE_ERROR")
209 }
210 EnvironmentFailureReason::ProtocolVersionMismatch => {
211 write!(f, "PROTOCOL_VERSION_MISMATCH")
212 }
213 EnvironmentFailureReason::UncaughtException => {
214 write!(f, "UNCAUGHT_EXCEPTION")
215 }
216 EnvironmentFailureReason::ForcedShutdown => {
217 write!(f, "FORCED_SHUTDOWN")
218 }
219 EnvironmentFailureReason::RecoveryFailure => {
220 write!(f, "RECOVERY_FAILURE")
221 }
222 EnvironmentFailureReason::Other(s) => write!(f, "{s}"),
223 }
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
234pub enum ExceptionSource {
235 Checkpointer,
236 Cleaner,
237 Evictor,
238 INCompressor,
239 Verifier,
240 ReplicationThread,
241 Unknown(String),
242}
243
244impl std::fmt::Display for ExceptionSource {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 match self {
247 ExceptionSource::Checkpointer => write!(f, "Checkpointer"),
248 ExceptionSource::Cleaner => write!(f, "Cleaner"),
249 ExceptionSource::Evictor => write!(f, "Evictor"),
250 ExceptionSource::INCompressor => write!(f, "INCompressor"),
251 ExceptionSource::Verifier => write!(f, "Verifier"),
252 ExceptionSource::ReplicationThread => {
253 write!(f, "ReplicationThread")
254 }
255 ExceptionSource::Unknown(s) => write!(f, "{s}"),
256 }
257 }
258}
259
260#[derive(Debug, Clone)]
264pub struct ExceptionEvent {
265 pub message: String,
267 pub source: ExceptionSource,
269 pub thread_name: String,
271}
272
273impl ExceptionEvent {
274 pub fn new(
276 message: impl Into<String>,
277 source: ExceptionSource,
278 thread_name: impl Into<String>,
279 ) -> Self {
280 Self {
281 message: message.into(),
282 source,
283 thread_name: thread_name.into(),
284 }
285 }
286}
287
288pub trait ExceptionListener: Send + Sync {
296 fn exception_event(&self, event: &ExceptionEvent);
297}
298
299#[derive(Debug, Error)]
313#[non_exhaustive]
314pub enum NoxuError {
315 #[error("environment failure ({reason}): {msg}")]
322 EnvironmentFailure {
323 reason: EnvironmentFailureReason,
325 msg: String,
327 },
328
329 #[error("environment wedged (permanent failure): {0}")]
334 EnvironmentWedged(String),
335
336 #[error("environment not found: {0}")]
340 EnvironmentNotFound(String),
341
342 #[error("environment locked by another process: {0}")]
346 EnvironmentLocked(String),
347
348 #[error("log write failure: {0}")]
352 LogWriteFailure(String),
353
354 #[error("disk limit exceeded: used={used}, limit={limit}")]
358 DiskLimitExceeded {
359 used: u64,
361 limit: u64,
363 },
364
365 #[error("thread interrupted during database operation")]
369 ThreadInterrupted,
370
371 #[error("database not found: {0}")]
374 DatabaseNotFound(String),
375
376 #[error("database already exists: {0}")]
380 DatabaseAlreadyExists(String),
381
382 #[error("database closed")]
384 DatabaseClosed,
385
386 #[error("environment closed")]
388 EnvironmentClosed,
389
390 #[error("cursor closed")]
392 CursorClosed,
393
394 #[error("lock conflict: {0}")]
399 LockConflict(String),
400
401 #[error("deadlock detected")]
405 DeadlockDetected,
406
407 #[error("lock timeout after {timeout_ms}ms{detail}")]
411 LockTimeout {
412 timeout_ms: u64,
414 #[allow(dead_code)]
417 detail: String,
418 },
419
420 #[error("lock not available (no-wait)")]
425 LockNotAvailable,
426
427 #[error("transaction timeout after {timeout_ms}ms for txn {txn_id}")]
431 TransactionTimeout {
432 timeout_ms: u64,
434 txn_id: i64,
436 },
437
438 #[error("lock preempted by higher-priority locker")]
442 LockPreempted,
443
444 #[error("transaction aborted: {0}")]
446 TransactionAborted(String),
447
448 #[error("key already exists")]
451 KeyExists,
452
453 #[error("unique constraint violated: {0}")]
457 UniqueConstraintViolation(String),
458
459 #[error("delete constraint violated: {0}")]
464 DeleteConstraintViolation(String),
465
466 #[error("foreign constraint violated: {0}")]
470 ForeignConstraintViolation(String),
471
472 #[error("duplicate data not allowed in no-dup-data operation")]
477 DuplicateDataException,
478
479 #[error("secondary integrity constraint violated: {0}")]
483 SecondaryIntegrityException(String),
484
485 #[error("sequence already exists: {0}")]
490 SequenceExists(String),
491
492 #[error("sequence not found: {0}")]
496 SequenceNotFound(String),
497
498 #[error("sequence overflow")]
502 SequenceOverflow,
503
504 #[error("sequence integrity violation: {0}")]
508 SequenceIntegrity(String),
509
510 #[error("not found")]
513 NotFound,
514
515 #[error("read-only mode")]
517 ReadOnly,
518
519 #[error("write not allowed on replica")]
524 ReplicaWrite,
525
526 #[error(
530 "insufficient replicas: required {required}, available {available}"
531 )]
532 InsufficientReplicas {
533 required: u32,
535 available: u32,
537 },
538
539 #[error("rollback required: {0}")]
543 RollbackRequired(String),
544
545 #[error("log checksum mismatch: {0}")]
550 LogChecksumMismatch(String),
551
552 #[error("log file not found: {0}")]
556 LogFileNotFound(String),
557
558 #[error("io error: {0}")]
560 IoError(#[from] std::io::Error),
561
562 #[error("version mismatch: {0}")]
567 VersionMismatch(String),
568
569 #[error("operation not allowed: {0}")]
574 OperationNotAllowed(String),
575
576 #[error("{msg}")]
585 OperationFailed {
586 msg: String,
588 #[source]
590 source: Box<dyn std::error::Error + Send + Sync + 'static>,
591 },
592
593 #[error("illegal argument: {0}")]
597 IllegalArgument(String),
598
599 #[error("operation timed out")]
601 Timeout,
602
603 #[error("invalid operation: {0}")]
605 InvalidOperation(String),
606
607 #[error("operation not yet supported: {0}")]
616 Unsupported(String),
617}
618
619impl NoxuError {
620 pub fn is_retryable(&self) -> bool {
627 matches!(
628 self,
629 NoxuError::LockConflict(_)
630 | NoxuError::DeadlockDetected
631 | NoxuError::LockTimeout { .. }
632 | NoxuError::LockNotAvailable
633 | NoxuError::TransactionTimeout { .. }
634 | NoxuError::LockPreempted
635 )
636 }
637
638 pub fn is_fatal_to_environment(&self) -> bool {
646 match self {
647 NoxuError::EnvironmentFailure { reason, .. } => {
648 reason.invalidates_environment()
649 }
650 NoxuError::LogChecksumMismatch(_)
651 | NoxuError::LogWriteFailure(_)
652 | NoxuError::DiskLimitExceeded { .. }
653 | NoxuError::EnvironmentWedged(_) => true,
654 _ => false,
655 }
656 }
657
658 pub fn reason(&self) -> Option<&EnvironmentFailureReason> {
663 match self {
664 NoxuError::EnvironmentFailure { reason, .. } => Some(reason),
665 _ => None,
666 }
667 }
668
669 pub fn is_corrupted(&self) -> bool {
673 match self {
674 NoxuError::EnvironmentFailure { reason, .. } => {
675 reason.is_corrupted()
676 }
677 NoxuError::LogChecksumMismatch(_) => true,
678 _ => false,
679 }
680 }
681
682 pub fn is_lock_conflict(&self) -> bool {
684 matches!(
685 self,
686 NoxuError::LockConflict(_)
687 | NoxuError::DeadlockDetected
688 | NoxuError::LockPreempted
689 | NoxuError::LockNotAvailable
690 )
691 }
692
693 pub fn is_lock_timeout(&self) -> bool {
695 matches!(
696 self,
697 NoxuError::LockTimeout { .. }
698 | NoxuError::TransactionTimeout { .. }
699 )
700 }
701
702 pub fn is_database_not_found(&self) -> bool {
704 matches!(self, NoxuError::DatabaseNotFound(_))
705 }
706
707 pub fn is_operation_failure(&self) -> bool {
709 self.is_retryable()
710 }
711
712 pub fn environment(msg: impl Into<String>) -> Self {
717 NoxuError::EnvironmentFailure {
718 reason: EnvironmentFailureReason::UnexpectedState,
719 msg: msg.into(),
720 }
721 }
722
723 pub fn environment_with_reason(
725 reason: EnvironmentFailureReason,
726 msg: impl Into<String>,
727 ) -> Self {
728 NoxuError::EnvironmentFailure { reason, msg: msg.into() }
729 }
730
731 pub fn database(msg: impl Into<String>) -> Self {
733 NoxuError::OperationNotAllowed(msg.into())
734 }
735
736 pub fn invalid_argument(msg: impl Into<String>) -> Self {
738 NoxuError::IllegalArgument(msg.into())
739 }
740
741 pub fn lock_conflict(msg: impl Into<String>) -> Self {
743 NoxuError::LockConflict(msg.into())
744 }
745
746 pub fn lock_timeout(timeout_ms: u64) -> Self {
748 NoxuError::LockTimeout { timeout_ms, detail: String::new() }
749 }
750
751 pub fn database_not_found(name: impl Into<String>) -> Self {
753 NoxuError::DatabaseNotFound(name.into())
754 }
755
756 pub fn disk_limit_exceeded(used: u64, limit: u64) -> Self {
758 NoxuError::DiskLimitExceeded { used, limit }
759 }
760}
761
762impl From<noxu_dbi::DbiError> for NoxuError {
765 fn from(e: noxu_dbi::DbiError) -> Self {
766 use noxu_dbi::DbiError;
767 match e {
768 DbiError::DatabaseNotFound(s) => NoxuError::DatabaseNotFound(s),
769 DbiError::DatabaseAlreadyExists(s)
770 | DbiError::DatabaseExists(s) => {
771 NoxuError::DatabaseAlreadyExists(s)
772 }
773 DbiError::EnvironmentFailure { reason } => {
774 NoxuError::EnvironmentFailure {
775 reason: EnvironmentFailureReason::UnexpectedState,
776 msg: reason,
777 }
778 }
779 DbiError::RecoveryFailure { reason } => {
786 NoxuError::EnvironmentFailure {
787 reason: EnvironmentFailureReason::RecoveryFailure,
788 msg: reason,
789 }
790 }
791 DbiError::EnvironmentNotOpen | DbiError::EnvironmentLocked(_) => {
792 NoxuError::EnvironmentClosed
793 }
794 DbiError::CursorClosed | DbiError::CursorNotInitialized => {
795 NoxuError::CursorClosed
796 }
797 DbiError::LockConflict(s) => NoxuError::LockConflict(s),
798 DbiError::DiskLimitExceeded { used, limit } => {
799 NoxuError::DiskLimitExceeded { used, limit }
800 }
801 DbiError::IoError(io) => NoxuError::IoError(io),
802 DbiError::TxnError(txn_err) => NoxuError::from(txn_err),
803 DbiError::LogError(log_err) => NoxuError::OperationFailed {
804 msg: log_err.to_string(),
805 source: Box::new(log_err),
806 },
807 DbiError::TreeError(tree_err) => NoxuError::OperationFailed {
808 msg: tree_err.to_string(),
809 source: Box::new(tree_err),
810 },
811 DbiError::DatabaseInUse(s) => NoxuError::OperationNotAllowed(s),
812 DbiError::OperationFailed(s) => NoxuError::OperationNotAllowed(s),
813 m @ DbiError::ComparatorMismatch { .. } => {
817 NoxuError::OperationFailed {
818 msg: m.to_string(),
819 source: Box::new(m),
820 }
821 }
822 }
823 }
824}
825
826impl From<noxu_txn::TxnError> for NoxuError {
827 fn from(e: noxu_txn::TxnError) -> Self {
828 use noxu_txn::TxnError;
829 match e {
830 TxnError::Deadlock(_) => NoxuError::DeadlockDetected,
831 TxnError::LockConflict(s) => NoxuError::LockConflict(s),
832 TxnError::LockTimeout {
833 timeout_ms,
834 ref owner,
835 ref requester,
836 ref requested_type,
837 lsn,
838 } => NoxuError::LockTimeout {
839 timeout_ms,
840 detail: format!(
841 " on LSN {lsn}: held by {owner}, \
842 requested {requested_type:?} by locker {requester}"
843 ),
844 },
845 TxnError::TransactionTimeout { timeout_ms, txn_id } => {
846 NoxuError::TransactionTimeout { timeout_ms, txn_id }
847 }
848 TxnError::LockNotAvailable { .. } => NoxuError::LockNotAvailable,
849 TxnError::RangeRestart => {
850 NoxuError::LockConflict("range restart".into())
851 }
852 TxnError::IllegalUpgrade { held, requested } => {
853 NoxuError::TransactionAborted(format!(
856 "illegal lock upgrade: held {held:?}, requested {requested:?}"
857 ))
858 }
859 TxnError::InvalidTransaction { txn_id, state } => {
860 NoxuError::TransactionAborted(format!("txn {txn_id}: {state}"))
861 }
862 TxnError::StateError(s) => NoxuError::TransactionAborted(s),
863 TxnError::LogError(log_err) => NoxuError::OperationFailed {
864 msg: log_err.to_string(),
865 source: Box::new(log_err),
866 },
867 }
868 }
869}
870
871pub type Result<T> = std::result::Result<T, NoxuError>;
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877
878 #[test]
879 fn test_error_display() {
880 let err = NoxuError::DatabaseNotFound("test_db".to_string());
881 assert_eq!(err.to_string(), "database not found: test_db");
882 }
883
884 #[test]
885 fn test_environment_failure() {
886 let err = NoxuError::environment("disk full");
887 assert!(err.to_string().contains("environment failure"));
888 assert!(err.to_string().contains("disk full"));
889 }
890
891 #[test]
892 fn test_environment_failure_with_reason() {
893 let err = NoxuError::environment_with_reason(
894 EnvironmentFailureReason::LogChecksum,
895 "checksum mismatch in file 7",
896 );
897 assert!(err.to_string().contains("LOG_CHECKSUM"));
898 assert!(err.is_corrupted());
899 assert!(err.is_fatal_to_environment());
900 assert_eq!(err.reason(), Some(&EnvironmentFailureReason::LogChecksum));
901 }
902
903 #[test]
904 fn test_environment_failure_reason_invalidates() {
905 assert!(
906 EnvironmentFailureReason::LogChecksum.invalidates_environment()
907 );
908 assert!(
909 EnvironmentFailureReason::BtreeCorruption.invalidates_environment()
910 );
911 assert!(EnvironmentFailureReason::DiskLimit.invalidates_environment());
912 assert!(
913 !EnvironmentFailureReason::UnexpectedState
914 .invalidates_environment()
915 );
916 assert!(
917 !EnvironmentFailureReason::UnexpectedException
918 .invalidates_environment()
919 );
920 }
921
922 #[test]
923 fn test_environment_failure_reason_corrupted() {
924 assert!(EnvironmentFailureReason::LogChecksum.is_corrupted());
925 assert!(EnvironmentFailureReason::BtreeCorruption.is_corrupted());
926 assert!(!EnvironmentFailureReason::DiskLimit.is_corrupted());
927 assert!(!EnvironmentFailureReason::LogWrite.is_corrupted());
928 }
929
930 #[test]
931 fn test_deadlock_detected() {
932 let err = NoxuError::DeadlockDetected;
933 assert_eq!(err.to_string(), "deadlock detected");
934 }
935
936 #[test]
937 fn test_cursor_closed() {
938 let err = NoxuError::CursorClosed;
939 assert_eq!(err.to_string(), "cursor closed");
940 }
941
942 #[test]
943 fn test_io_error_conversion() {
944 let io_err =
945 std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
946 let err: NoxuError = io_err.into();
947 assert!(matches!(err, NoxuError::IoError(_)));
948 }
949
950 #[test]
951 fn test_result_type() {
952 let ok_result: Result<i32> = Ok(42);
953 assert!(ok_result.is_ok_and(|v| v == 42));
954
955 let err_result: Result<i32> = Err(NoxuError::NotFound);
956 assert!(err_result.is_err());
957 }
958
959 #[test]
960 fn test_not_found() {
961 let err = NoxuError::NotFound;
962 assert_eq!(err.to_string(), "not found");
963 }
964
965 #[test]
966 fn test_key_exists() {
967 let err = NoxuError::KeyExists;
968 assert_eq!(err.to_string(), "key already exists");
969 }
970
971 #[test]
972 fn test_timeout() {
973 let err = NoxuError::Timeout;
974 assert_eq!(err.to_string(), "operation timed out");
975 }
976
977 #[test]
978 fn test_from_dbi_error() {
979 let dbi_err = noxu_dbi::DbiError::CursorClosed;
980 let err: NoxuError = NoxuError::from(dbi_err);
981 assert!(matches!(err, NoxuError::CursorClosed));
982
983 let e: NoxuError =
984 noxu_dbi::DbiError::DatabaseNotFound("x".into()).into();
985 assert!(matches!(e, NoxuError::DatabaseNotFound(_)));
986
987 let e: NoxuError =
988 noxu_dbi::DbiError::EnvironmentFailure { reason: "disk".into() }
989 .into();
990 assert!(matches!(e, NoxuError::EnvironmentFailure { .. }));
991 }
992
993 #[test]
994 fn test_from_txn_error() {
995 use noxu_txn::{LockType, TxnError};
996
997 let e: NoxuError = TxnError::Deadlock("cycle".into()).into();
998 assert!(matches!(e, NoxuError::DeadlockDetected));
999
1000 let e: NoxuError = TxnError::LockTimeout {
1001 timeout_ms: 500,
1002 lsn: 1,
1003 owner: "t1".into(),
1004 requested_type: LockType::Write,
1005 requester: "t2".into(),
1006 }
1007 .into();
1008 assert!(matches!(e, NoxuError::LockTimeout { timeout_ms: 500, .. }));
1009
1010 let e: NoxuError =
1011 TxnError::TransactionTimeout { timeout_ms: 1000, txn_id: 42 }
1012 .into();
1013 assert!(matches!(
1014 e,
1015 NoxuError::TransactionTimeout { timeout_ms: 1000, txn_id: 42 }
1016 ));
1017
1018 let e: NoxuError = TxnError::LockNotAvailable { lsn: 0 }.into();
1020 assert!(matches!(e, NoxuError::LockNotAvailable));
1021 }
1022
1023 #[test]
1024 fn test_is_retryable() {
1025 assert!(NoxuError::DeadlockDetected.is_retryable());
1026 assert!(NoxuError::LockConflict("x".into()).is_retryable());
1027 assert!(
1028 NoxuError::LockTimeout { timeout_ms: 500, detail: String::new() }
1029 .is_retryable()
1030 );
1031 assert!(
1032 NoxuError::TransactionTimeout { timeout_ms: 1000, txn_id: 1 }
1033 .is_retryable()
1034 );
1035 assert!(NoxuError::LockPreempted.is_retryable());
1036 assert!(NoxuError::LockNotAvailable.is_retryable());
1037
1038 assert!(!NoxuError::NotFound.is_retryable());
1039 assert!(!NoxuError::environment("x").is_retryable());
1040 assert!(!NoxuError::DatabaseClosed.is_retryable());
1041 }
1042
1043 #[test]
1044 fn test_is_fatal_to_environment() {
1045 assert!(
1046 NoxuError::environment_with_reason(
1047 EnvironmentFailureReason::LogChecksum,
1048 "x"
1049 )
1050 .is_fatal_to_environment()
1051 );
1052 assert!(
1053 NoxuError::LogChecksumMismatch("bad".into())
1054 .is_fatal_to_environment()
1055 );
1056 assert!(
1057 NoxuError::LogWriteFailure("io".into()).is_fatal_to_environment()
1058 );
1059 assert!(
1060 NoxuError::DiskLimitExceeded { used: 100, limit: 50 }
1061 .is_fatal_to_environment()
1062 );
1063 assert!(
1064 NoxuError::EnvironmentWedged("x".into()).is_fatal_to_environment()
1065 );
1066
1067 assert!(
1069 !NoxuError::environment_with_reason(
1070 EnvironmentFailureReason::UnexpectedState,
1071 "x"
1072 )
1073 .is_fatal_to_environment()
1074 );
1075
1076 assert!(!NoxuError::DeadlockDetected.is_fatal_to_environment());
1077 assert!(!NoxuError::NotFound.is_fatal_to_environment());
1078 assert!(!NoxuError::LockConflict("x".into()).is_fatal_to_environment());
1079 }
1080
1081 #[test]
1082 fn test_new_variants() {
1083 let e =
1084 NoxuError::LockTimeout { timeout_ms: 250, detail: String::new() };
1085 assert!(e.to_string().contains("250ms"));
1086
1087 let e = NoxuError::TransactionTimeout { timeout_ms: 1000, txn_id: 7 };
1088 assert!(e.to_string().contains("1000ms"));
1089 assert!(e.to_string().contains("7"));
1090
1091 let e = NoxuError::LockPreempted;
1092 assert!(e.to_string().contains("preempted"));
1093
1094 let e = NoxuError::LockNotAvailable;
1095 assert!(e.to_string().contains("no-wait"));
1096
1097 let e = NoxuError::UniqueConstraintViolation("idx_email".into());
1098 assert!(e.to_string().contains("unique constraint"));
1099
1100 let e = NoxuError::DeleteConstraintViolation("key=42".into());
1101 assert!(e.to_string().contains("delete constraint"));
1102
1103 let e = NoxuError::ForeignConstraintViolation("fk_user".into());
1104 assert!(e.to_string().contains("foreign constraint"));
1105
1106 let e = NoxuError::DuplicateDataException;
1107 assert!(e.to_string().contains("duplicate data"));
1108
1109 let e = NoxuError::ReplicaWrite;
1110 assert!(e.to_string().contains("replica"));
1111
1112 let e = NoxuError::InsufficientReplicas { required: 3, available: 1 };
1113 assert!(e.to_string().contains("required 3"));
1114 assert!(e.to_string().contains("available 1"));
1115
1116 let e = NoxuError::RollbackRequired("ha failover".into());
1117 assert!(e.to_string().contains("rollback required"));
1118
1119 let e = NoxuError::LogChecksumMismatch("file 7".into());
1120 assert!(e.to_string().contains("log checksum mismatch"));
1121
1122 let e = NoxuError::LogFileNotFound("00000007.ndb".into());
1123 assert!(e.to_string().contains("log file not found"));
1124
1125 let e = NoxuError::EnvironmentWedged("perm fail".into());
1126 assert!(e.to_string().contains("permanent failure"));
1127
1128 let e = NoxuError::EnvironmentNotFound("/bad/path".into());
1129 assert!(e.to_string().contains("not found"));
1130
1131 let e = NoxuError::EnvironmentLocked("/data".into());
1132 assert!(e.to_string().contains("locked"));
1133
1134 let e = NoxuError::LogWriteFailure("ENOSPC".into());
1135 assert!(e.to_string().contains("log write failure"));
1136
1137 let e = NoxuError::DiskLimitExceeded { used: 1000, limit: 500 };
1138 assert!(e.to_string().contains("used=1000"));
1139 assert!(e.to_string().contains("limit=500"));
1140
1141 let e = NoxuError::ThreadInterrupted;
1142 assert!(e.to_string().contains("interrupted"));
1143 }
1144
1145 #[test]
1146 fn test_sequence_variants() {
1147 let e = NoxuError::SequenceExists("counter".into());
1148 assert!(e.to_string().contains("counter"));
1149
1150 let e = NoxuError::SequenceNotFound("counter".into());
1151 assert!(e.to_string().contains("sequence not found"));
1152
1153 let e = NoxuError::SequenceOverflow;
1154 assert!(e.to_string().contains("overflow"));
1155
1156 let e = NoxuError::SequenceIntegrity("bad state".into());
1157 assert!(e.to_string().contains("integrity"));
1158 }
1159
1160 #[test]
1161 fn test_database_already_exists() {
1162 let e = NoxuError::DatabaseAlreadyExists("mydb".into());
1163 assert!(e.to_string().contains("mydb"));
1164 }
1165
1166 #[test]
1167 fn test_lock_conflict() {
1168 let e = NoxuError::LockConflict("timeout".into());
1169 assert!(e.to_string().contains("lock conflict"));
1170 }
1171
1172 #[test]
1173 fn test_transaction_aborted() {
1174 let e = NoxuError::TransactionAborted("rolled back".into());
1175 assert!(e.to_string().contains("transaction aborted"));
1176 }
1177
1178 #[test]
1179 fn test_helpers() {
1180 assert!(matches!(
1181 NoxuError::environment("x"),
1182 NoxuError::EnvironmentFailure { .. }
1183 ));
1184 assert!(matches!(
1185 NoxuError::database("x"),
1186 NoxuError::OperationNotAllowed(_)
1187 ));
1188 assert!(matches!(
1189 NoxuError::invalid_argument("x"),
1190 NoxuError::IllegalArgument(_)
1191 ));
1192 assert!(matches!(
1193 NoxuError::lock_conflict("x"),
1194 NoxuError::LockConflict(_)
1195 ));
1196 assert!(matches!(
1197 NoxuError::lock_timeout(500),
1198 NoxuError::LockTimeout { timeout_ms: 500, .. }
1199 ));
1200 assert!(matches!(
1201 NoxuError::database_not_found("db"),
1202 NoxuError::DatabaseNotFound(_)
1203 ));
1204 assert!(matches!(
1205 NoxuError::disk_limit_exceeded(100, 50),
1206 NoxuError::DiskLimitExceeded { used: 100, limit: 50 }
1207 ));
1208 }
1209
1210 #[test]
1211 fn test_is_lock_conflict() {
1212 assert!(NoxuError::LockConflict("x".into()).is_lock_conflict());
1213 assert!(NoxuError::DeadlockDetected.is_lock_conflict());
1214 assert!(NoxuError::LockPreempted.is_lock_conflict());
1215 assert!(NoxuError::LockNotAvailable.is_lock_conflict());
1216 assert!(
1217 !NoxuError::LockTimeout { timeout_ms: 500, detail: String::new() }
1218 .is_lock_conflict()
1219 );
1220 assert!(!NoxuError::NotFound.is_lock_conflict());
1221 }
1222
1223 #[test]
1224 fn test_is_lock_timeout() {
1225 assert!(
1226 NoxuError::LockTimeout { timeout_ms: 500, detail: String::new() }
1227 .is_lock_timeout()
1228 );
1229 assert!(
1230 NoxuError::TransactionTimeout { timeout_ms: 1000, txn_id: 1 }
1231 .is_lock_timeout()
1232 );
1233 assert!(!NoxuError::LockConflict("x".into()).is_lock_timeout());
1234 assert!(!NoxuError::NotFound.is_lock_timeout());
1235 }
1236
1237 #[test]
1238 fn test_is_database_not_found() {
1239 assert!(
1240 NoxuError::DatabaseNotFound("mydb".into()).is_database_not_found()
1241 );
1242 assert!(!NoxuError::DatabaseClosed.is_database_not_found());
1243 assert!(!NoxuError::NotFound.is_database_not_found());
1244 }
1245
1246 #[test]
1247 fn test_exception_event() {
1248 let evt = ExceptionEvent::new(
1249 "cleaner failed",
1250 ExceptionSource::Cleaner,
1251 "cleaner-1",
1252 );
1253 assert_eq!(evt.message, "cleaner failed");
1254 assert_eq!(evt.source, ExceptionSource::Cleaner);
1255 assert_eq!(evt.thread_name, "cleaner-1");
1256 assert!(evt.source.to_string().contains("Cleaner"));
1257 }
1258
1259 #[test]
1260 fn test_exception_listener_trait() {
1261 struct NoopListener;
1262 impl ExceptionListener for NoopListener {
1263 fn exception_event(&self, _event: &ExceptionEvent) {}
1264 }
1265 let listener: Box<dyn ExceptionListener> = Box::new(NoopListener);
1266 let evt =
1267 ExceptionEvent::new("x", ExceptionSource::Checkpointer, "ckpt");
1268 listener.exception_event(&evt);
1269 }
1270
1271 #[test]
1272 fn test_operation_failed_preserves_source_chain() {
1273 use std::error::Error;
1274 let dbi_err = noxu_dbi::DbiError::ComparatorMismatch {
1278 name: "users".into(),
1279 kind: "btree",
1280 persisted: Some("a".into()),
1281 configured: Some("b".into()),
1282 };
1283 let display_before = dbi_err.to_string();
1284 let err: NoxuError = NoxuError::from(dbi_err);
1285 assert!(matches!(err, NoxuError::OperationFailed { .. }));
1286 assert_eq!(err.to_string(), display_before);
1288 let src = err.source().expect("source() must chain");
1290 assert_eq!(src.to_string(), display_before);
1291 }
1292
1293 #[test]
1294 fn test_reason_display() {
1295 assert_eq!(
1296 EnvironmentFailureReason::LogChecksum.to_string(),
1297 "LOG_CHECKSUM"
1298 );
1299 assert_eq!(
1300 EnvironmentFailureReason::BtreeCorruption.to_string(),
1301 "BTREE_CORRUPTION"
1302 );
1303 assert_eq!(
1304 EnvironmentFailureReason::DiskLimit.to_string(),
1305 "DISK_LIMIT"
1306 );
1307 assert_eq!(
1308 EnvironmentFailureReason::Other("CUSTOM".into()).to_string(),
1309 "CUSTOM"
1310 );
1311 }
1312}