1use thiserror::Error;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum EnvironmentFailureReason {
38 LogChecksum,
42
43 LogWrite,
45
46 LogFileNotFound,
49
50 LogIntegrity,
53
54 BtreeCorruption,
58
59 UnexpectedState,
63
64 UnexpectedStateFatal,
67
68 UnexpectedException,
71
72 UnexpectedExceptionFatal,
75
76 DiskLimit,
80
81 LatchTimeout,
83
84 ThreadInterrupted,
88
89 MasterToReplicaTransition,
93
94 ReplicaFencing,
97
98 HandshakeError,
101
102 ProtocolVersionMismatch,
105
106 UncaughtException,
109
110 ForcedShutdown,
113
114 RecoveryFailure,
125
126 Other(String),
128}
129
130impl EnvironmentFailureReason {
131 pub fn invalidates_environment(&self) -> bool {
138 matches!(
139 self,
140 EnvironmentFailureReason::LogChecksum
141 | EnvironmentFailureReason::LogWrite
142 | EnvironmentFailureReason::LogIntegrity
143 | EnvironmentFailureReason::BtreeCorruption
144 | EnvironmentFailureReason::UnexpectedStateFatal
145 | EnvironmentFailureReason::UnexpectedExceptionFatal
146 | EnvironmentFailureReason::RecoveryFailure
147 | EnvironmentFailureReason::DiskLimit
148 | EnvironmentFailureReason::LatchTimeout
149 | EnvironmentFailureReason::ReplicaFencing
150 | EnvironmentFailureReason::ForcedShutdown
151 )
152 }
153
154 pub fn is_corrupted(&self) -> bool {
159 matches!(
160 self,
161 EnvironmentFailureReason::LogChecksum
162 | EnvironmentFailureReason::BtreeCorruption
163 )
164 }
165}
166
167impl std::fmt::Display for EnvironmentFailureReason {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 match self {
170 EnvironmentFailureReason::LogChecksum => write!(f, "LOG_CHECKSUM"),
171 EnvironmentFailureReason::LogWrite => write!(f, "LOG_WRITE"),
172 EnvironmentFailureReason::LogFileNotFound => {
173 write!(f, "LOG_FILE_NOT_FOUND")
174 }
175 EnvironmentFailureReason::LogIntegrity => {
176 write!(f, "LOG_INTEGRITY")
177 }
178 EnvironmentFailureReason::BtreeCorruption => {
179 write!(f, "BTREE_CORRUPTION")
180 }
181 EnvironmentFailureReason::UnexpectedState => {
182 write!(f, "UNEXPECTED_STATE")
183 }
184 EnvironmentFailureReason::UnexpectedStateFatal => {
185 write!(f, "UNEXPECTED_STATE_FATAL")
186 }
187 EnvironmentFailureReason::UnexpectedException => {
188 write!(f, "UNEXPECTED_EXCEPTION")
189 }
190 EnvironmentFailureReason::UnexpectedExceptionFatal => {
191 write!(f, "UNEXPECTED_EXCEPTION_FATAL")
192 }
193 EnvironmentFailureReason::DiskLimit => write!(f, "DISK_LIMIT"),
194 EnvironmentFailureReason::LatchTimeout => {
195 write!(f, "LATCH_TIMEOUT")
196 }
197 EnvironmentFailureReason::ThreadInterrupted => {
198 write!(f, "THREAD_INTERRUPTED")
199 }
200 EnvironmentFailureReason::MasterToReplicaTransition => {
201 write!(f, "MASTER_TO_REPLICA_TRANSITION")
202 }
203 EnvironmentFailureReason::ReplicaFencing => {
204 write!(f, "REPLICA_FENCING")
205 }
206 EnvironmentFailureReason::HandshakeError => {
207 write!(f, "HANDSHAKE_ERROR")
208 }
209 EnvironmentFailureReason::ProtocolVersionMismatch => {
210 write!(f, "PROTOCOL_VERSION_MISMATCH")
211 }
212 EnvironmentFailureReason::UncaughtException => {
213 write!(f, "UNCAUGHT_EXCEPTION")
214 }
215 EnvironmentFailureReason::ForcedShutdown => {
216 write!(f, "FORCED_SHUTDOWN")
217 }
218 EnvironmentFailureReason::RecoveryFailure => {
219 write!(f, "RECOVERY_FAILURE")
220 }
221 EnvironmentFailureReason::Other(s) => write!(f, "{s}"),
222 }
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum ExceptionSource {
234 Checkpointer,
235 Cleaner,
236 Evictor,
237 INCompressor,
238 Verifier,
239 ReplicationThread,
240 Unknown(String),
241}
242
243impl std::fmt::Display for ExceptionSource {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 match self {
246 ExceptionSource::Checkpointer => write!(f, "Checkpointer"),
247 ExceptionSource::Cleaner => write!(f, "Cleaner"),
248 ExceptionSource::Evictor => write!(f, "Evictor"),
249 ExceptionSource::INCompressor => write!(f, "INCompressor"),
250 ExceptionSource::Verifier => write!(f, "Verifier"),
251 ExceptionSource::ReplicationThread => {
252 write!(f, "ReplicationThread")
253 }
254 ExceptionSource::Unknown(s) => write!(f, "{s}"),
255 }
256 }
257}
258
259#[derive(Debug, Clone)]
263pub struct ExceptionEvent {
264 pub message: String,
266 pub source: ExceptionSource,
268 pub thread_name: String,
270}
271
272impl ExceptionEvent {
273 pub fn new(
275 message: impl Into<String>,
276 source: ExceptionSource,
277 thread_name: impl Into<String>,
278 ) -> Self {
279 Self {
280 message: message.into(),
281 source,
282 thread_name: thread_name.into(),
283 }
284 }
285}
286
287pub trait ExceptionListener: Send + Sync {
295 fn exception_event(&self, event: &ExceptionEvent);
296}
297
298#[derive(Debug, Error)]
312pub enum NoxuError {
313 #[error("environment failure ({reason}): {msg}")]
320 EnvironmentFailure {
321 reason: EnvironmentFailureReason,
323 msg: String,
325 },
326
327 #[error("environment wedged (permanent failure): {0}")]
332 EnvironmentWedged(String),
333
334 #[error("environment not found: {0}")]
338 EnvironmentNotFound(String),
339
340 #[error("environment locked by another process: {0}")]
344 EnvironmentLocked(String),
345
346 #[error("log write failure: {0}")]
350 LogWriteFailure(String),
351
352 #[error("disk limit exceeded: used={used}, limit={limit}")]
356 DiskLimitExceeded {
357 used: u64,
359 limit: u64,
361 },
362
363 #[error("thread interrupted during database operation")]
367 ThreadInterrupted,
368
369 #[error("database not found: {0}")]
372 DatabaseNotFound(String),
373
374 #[error("database already exists: {0}")]
378 DatabaseAlreadyExists(String),
379
380 #[error("database closed")]
382 DatabaseClosed,
383
384 #[error("environment closed")]
386 EnvironmentClosed,
387
388 #[error("cursor closed")]
390 CursorClosed,
391
392 #[error("lock conflict: {0}")]
397 LockConflict(String),
398
399 #[error("deadlock detected")]
403 DeadlockDetected,
404
405 #[error("lock timeout after {timeout_ms}ms{detail}")]
409 LockTimeout {
410 timeout_ms: u64,
412 #[allow(dead_code)]
415 detail: String,
416 },
417
418 #[error("lock not available (no-wait)")]
423 LockNotAvailable,
424
425 #[error("transaction timeout after {timeout_ms}ms for txn {txn_id}")]
429 TransactionTimeout {
430 timeout_ms: u64,
432 txn_id: i64,
434 },
435
436 #[error("lock preempted by higher-priority locker")]
440 LockPreempted,
441
442 #[error("transaction aborted: {0}")]
444 TransactionAborted(String),
445
446 #[error("key already exists")]
449 KeyExists,
450
451 #[error("unique constraint violated: {0}")]
455 UniqueConstraintViolation(String),
456
457 #[error("delete constraint violated: {0}")]
462 DeleteConstraintViolation(String),
463
464 #[error("foreign constraint violated: {0}")]
468 ForeignConstraintViolation(String),
469
470 #[error("duplicate data not allowed in no-dup-data operation")]
475 DuplicateDataException,
476
477 #[error("secondary integrity constraint violated: {0}")]
481 SecondaryIntegrityException(String),
482
483 #[error("sequence already exists: {0}")]
488 SequenceExists(String),
489
490 #[error("sequence not found: {0}")]
494 SequenceNotFound(String),
495
496 #[error("sequence overflow")]
500 SequenceOverflow,
501
502 #[error("sequence integrity violation: {0}")]
506 SequenceIntegrity(String),
507
508 #[error("not found")]
511 NotFound,
512
513 #[error("read-only mode")]
515 ReadOnly,
516
517 #[error("write not allowed on replica")]
522 ReplicaWrite,
523
524 #[error(
528 "insufficient replicas: required {required}, available {available}"
529 )]
530 InsufficientReplicas {
531 required: u32,
533 available: u32,
535 },
536
537 #[error("rollback required: {0}")]
541 RollbackRequired(String),
542
543 #[error("log checksum mismatch: {0}")]
548 LogChecksumMismatch(String),
549
550 #[error("log file not found: {0}")]
554 LogFileNotFound(String),
555
556 #[error("io error: {0}")]
558 IoError(#[from] std::io::Error),
559
560 #[error("version mismatch: {0}")]
565 VersionMismatch(String),
566
567 #[error("operation not allowed: {0}")]
572 OperationNotAllowed(String),
573
574 #[error("illegal argument: {0}")]
578 IllegalArgument(String),
579
580 #[error("operation timed out")]
582 Timeout,
583
584 #[error("invalid operation: {0}")]
586 InvalidOperation(String),
587
588 #[error("operation not yet supported: {0}")]
597 Unsupported(String),
598}
599
600impl NoxuError {
601 pub fn is_retryable(&self) -> bool {
608 matches!(
609 self,
610 NoxuError::LockConflict(_)
611 | NoxuError::DeadlockDetected
612 | NoxuError::LockTimeout { .. }
613 | NoxuError::LockNotAvailable
614 | NoxuError::TransactionTimeout { .. }
615 | NoxuError::LockPreempted
616 )
617 }
618
619 pub fn is_fatal_to_environment(&self) -> bool {
627 match self {
628 NoxuError::EnvironmentFailure { reason, .. } => {
629 reason.invalidates_environment()
630 }
631 NoxuError::LogChecksumMismatch(_)
632 | NoxuError::LogWriteFailure(_)
633 | NoxuError::DiskLimitExceeded { .. }
634 | NoxuError::EnvironmentWedged(_) => true,
635 _ => false,
636 }
637 }
638
639 pub fn reason(&self) -> Option<&EnvironmentFailureReason> {
644 match self {
645 NoxuError::EnvironmentFailure { reason, .. } => Some(reason),
646 _ => None,
647 }
648 }
649
650 pub fn is_corrupted(&self) -> bool {
654 match self {
655 NoxuError::EnvironmentFailure { reason, .. } => {
656 reason.is_corrupted()
657 }
658 NoxuError::LogChecksumMismatch(_) => true,
659 _ => false,
660 }
661 }
662
663 pub fn is_lock_conflict(&self) -> bool {
665 matches!(
666 self,
667 NoxuError::LockConflict(_)
668 | NoxuError::DeadlockDetected
669 | NoxuError::LockPreempted
670 | NoxuError::LockNotAvailable
671 )
672 }
673
674 pub fn is_lock_timeout(&self) -> bool {
676 matches!(
677 self,
678 NoxuError::LockTimeout { .. }
679 | NoxuError::TransactionTimeout { .. }
680 )
681 }
682
683 pub fn is_database_not_found(&self) -> bool {
685 matches!(self, NoxuError::DatabaseNotFound(_))
686 }
687
688 pub fn is_operation_failure(&self) -> bool {
690 self.is_retryable()
691 }
692
693 pub fn environment(msg: impl Into<String>) -> Self {
698 NoxuError::EnvironmentFailure {
699 reason: EnvironmentFailureReason::UnexpectedState,
700 msg: msg.into(),
701 }
702 }
703
704 pub fn environment_with_reason(
706 reason: EnvironmentFailureReason,
707 msg: impl Into<String>,
708 ) -> Self {
709 NoxuError::EnvironmentFailure { reason, msg: msg.into() }
710 }
711
712 pub fn database(msg: impl Into<String>) -> Self {
714 NoxuError::OperationNotAllowed(msg.into())
715 }
716
717 pub fn invalid_argument(msg: impl Into<String>) -> Self {
719 NoxuError::IllegalArgument(msg.into())
720 }
721
722 pub fn lock_conflict(msg: impl Into<String>) -> Self {
724 NoxuError::LockConflict(msg.into())
725 }
726
727 pub fn lock_timeout(timeout_ms: u64) -> Self {
729 NoxuError::LockTimeout { timeout_ms, detail: String::new() }
730 }
731
732 pub fn database_not_found(name: impl Into<String>) -> Self {
734 NoxuError::DatabaseNotFound(name.into())
735 }
736
737 pub fn disk_limit_exceeded(used: u64, limit: u64) -> Self {
739 NoxuError::DiskLimitExceeded { used, limit }
740 }
741}
742
743impl From<noxu_dbi::DbiError> for NoxuError {
746 fn from(e: noxu_dbi::DbiError) -> Self {
747 use noxu_dbi::DbiError;
748 match e {
749 DbiError::DatabaseNotFound(s) => NoxuError::DatabaseNotFound(s),
750 DbiError::DatabaseAlreadyExists(s)
751 | DbiError::DatabaseExists(s) => {
752 NoxuError::DatabaseAlreadyExists(s)
753 }
754 DbiError::EnvironmentFailure { reason } => {
755 NoxuError::EnvironmentFailure {
756 reason: EnvironmentFailureReason::UnexpectedState,
757 msg: reason,
758 }
759 }
760 DbiError::RecoveryFailure { reason } => {
767 NoxuError::EnvironmentFailure {
768 reason: EnvironmentFailureReason::RecoveryFailure,
769 msg: reason,
770 }
771 }
772 DbiError::EnvironmentNotOpen | DbiError::EnvironmentLocked(_) => {
773 NoxuError::EnvironmentClosed
774 }
775 DbiError::CursorClosed | DbiError::CursorNotInitialized => {
776 NoxuError::CursorClosed
777 }
778 DbiError::LockConflict(s) => NoxuError::LockConflict(s),
779 DbiError::IoError(io) => NoxuError::IoError(io),
780 DbiError::TxnError(txn_err) => NoxuError::from(txn_err),
781 DbiError::LogError(log_err) => {
782 NoxuError::OperationNotAllowed(log_err.to_string())
783 }
784 DbiError::TreeError(tree_err) => {
785 NoxuError::OperationNotAllowed(tree_err.to_string())
786 }
787 DbiError::DatabaseInUse(s) => NoxuError::OperationNotAllowed(s),
788 DbiError::OperationFailed(s) => NoxuError::OperationNotAllowed(s),
789 }
790 }
791}
792
793impl From<noxu_txn::TxnError> for NoxuError {
794 fn from(e: noxu_txn::TxnError) -> Self {
795 use noxu_txn::TxnError;
796 match e {
797 TxnError::Deadlock(_) => NoxuError::DeadlockDetected,
798 TxnError::LockConflict(s) => NoxuError::LockConflict(s),
799 TxnError::LockTimeout {
800 timeout_ms,
801 ref owner,
802 ref requester,
803 ref requested_type,
804 lsn,
805 } => NoxuError::LockTimeout {
806 timeout_ms,
807 detail: format!(
808 " on LSN {lsn}: held by {owner}, \
809 requested {requested_type:?} by locker {requester}"
810 ),
811 },
812 TxnError::TransactionTimeout { timeout_ms, txn_id } => {
813 NoxuError::TransactionTimeout { timeout_ms, txn_id }
814 }
815 TxnError::LockNotAvailable { .. } => NoxuError::LockNotAvailable,
816 TxnError::RangeRestart => {
817 NoxuError::LockConflict("range restart".into())
818 }
819 TxnError::InvalidTransaction { txn_id, state } => {
820 NoxuError::TransactionAborted(format!("txn {txn_id}: {state}"))
821 }
822 TxnError::StateError(s) => NoxuError::TransactionAborted(s),
823 TxnError::LogError(log_err) => {
824 NoxuError::OperationNotAllowed(log_err.to_string())
825 }
826 }
827 }
828}
829
830pub type Result<T> = std::result::Result<T, NoxuError>;
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn test_error_display() {
839 let err = NoxuError::DatabaseNotFound("test_db".to_string());
840 assert_eq!(err.to_string(), "database not found: test_db");
841 }
842
843 #[test]
844 fn test_environment_failure() {
845 let err = NoxuError::environment("disk full");
846 assert!(err.to_string().contains("environment failure"));
847 assert!(err.to_string().contains("disk full"));
848 }
849
850 #[test]
851 fn test_environment_failure_with_reason() {
852 let err = NoxuError::environment_with_reason(
853 EnvironmentFailureReason::LogChecksum,
854 "checksum mismatch in file 7",
855 );
856 assert!(err.to_string().contains("LOG_CHECKSUM"));
857 assert!(err.is_corrupted());
858 assert!(err.is_fatal_to_environment());
859 assert_eq!(err.reason(), Some(&EnvironmentFailureReason::LogChecksum));
860 }
861
862 #[test]
863 fn test_environment_failure_reason_invalidates() {
864 assert!(
865 EnvironmentFailureReason::LogChecksum.invalidates_environment()
866 );
867 assert!(
868 EnvironmentFailureReason::BtreeCorruption.invalidates_environment()
869 );
870 assert!(EnvironmentFailureReason::DiskLimit.invalidates_environment());
871 assert!(
872 !EnvironmentFailureReason::UnexpectedState
873 .invalidates_environment()
874 );
875 assert!(
876 !EnvironmentFailureReason::UnexpectedException
877 .invalidates_environment()
878 );
879 }
880
881 #[test]
882 fn test_environment_failure_reason_corrupted() {
883 assert!(EnvironmentFailureReason::LogChecksum.is_corrupted());
884 assert!(EnvironmentFailureReason::BtreeCorruption.is_corrupted());
885 assert!(!EnvironmentFailureReason::DiskLimit.is_corrupted());
886 assert!(!EnvironmentFailureReason::LogWrite.is_corrupted());
887 }
888
889 #[test]
890 fn test_deadlock_detected() {
891 let err = NoxuError::DeadlockDetected;
892 assert_eq!(err.to_string(), "deadlock detected");
893 }
894
895 #[test]
896 fn test_cursor_closed() {
897 let err = NoxuError::CursorClosed;
898 assert_eq!(err.to_string(), "cursor closed");
899 }
900
901 #[test]
902 fn test_io_error_conversion() {
903 let io_err =
904 std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
905 let err: NoxuError = io_err.into();
906 assert!(matches!(err, NoxuError::IoError(_)));
907 }
908
909 #[test]
910 fn test_result_type() {
911 let ok_result: Result<i32> = Ok(42);
912 assert!(ok_result.is_ok_and(|v| v == 42));
913
914 let err_result: Result<i32> = Err(NoxuError::NotFound);
915 assert!(err_result.is_err());
916 }
917
918 #[test]
919 fn test_not_found() {
920 let err = NoxuError::NotFound;
921 assert_eq!(err.to_string(), "not found");
922 }
923
924 #[test]
925 fn test_key_exists() {
926 let err = NoxuError::KeyExists;
927 assert_eq!(err.to_string(), "key already exists");
928 }
929
930 #[test]
931 fn test_timeout() {
932 let err = NoxuError::Timeout;
933 assert_eq!(err.to_string(), "operation timed out");
934 }
935
936 #[test]
937 fn test_from_dbi_error() {
938 let dbi_err = noxu_dbi::DbiError::CursorClosed;
939 let err: NoxuError = NoxuError::from(dbi_err);
940 assert!(matches!(err, NoxuError::CursorClosed));
941
942 let e: NoxuError =
943 noxu_dbi::DbiError::DatabaseNotFound("x".into()).into();
944 assert!(matches!(e, NoxuError::DatabaseNotFound(_)));
945
946 let e: NoxuError =
947 noxu_dbi::DbiError::EnvironmentFailure { reason: "disk".into() }
948 .into();
949 assert!(matches!(e, NoxuError::EnvironmentFailure { .. }));
950 }
951
952 #[test]
953 fn test_from_txn_error() {
954 use noxu_txn::{LockType, TxnError};
955
956 let e: NoxuError = TxnError::Deadlock("cycle".into()).into();
957 assert!(matches!(e, NoxuError::DeadlockDetected));
958
959 let e: NoxuError = TxnError::LockTimeout {
960 timeout_ms: 500,
961 lsn: 1,
962 owner: "t1".into(),
963 requested_type: LockType::Write,
964 requester: "t2".into(),
965 }
966 .into();
967 assert!(matches!(e, NoxuError::LockTimeout { timeout_ms: 500, .. }));
968
969 let e: NoxuError =
970 TxnError::TransactionTimeout { timeout_ms: 1000, txn_id: 42 }
971 .into();
972 assert!(matches!(
973 e,
974 NoxuError::TransactionTimeout { timeout_ms: 1000, txn_id: 42 }
975 ));
976
977 let e: NoxuError = TxnError::LockNotAvailable { lsn: 0 }.into();
979 assert!(matches!(e, NoxuError::LockNotAvailable));
980 }
981
982 #[test]
983 fn test_is_retryable() {
984 assert!(NoxuError::DeadlockDetected.is_retryable());
985 assert!(NoxuError::LockConflict("x".into()).is_retryable());
986 assert!(
987 NoxuError::LockTimeout { timeout_ms: 500, detail: String::new() }
988 .is_retryable()
989 );
990 assert!(
991 NoxuError::TransactionTimeout { timeout_ms: 1000, txn_id: 1 }
992 .is_retryable()
993 );
994 assert!(NoxuError::LockPreempted.is_retryable());
995 assert!(NoxuError::LockNotAvailable.is_retryable());
996
997 assert!(!NoxuError::NotFound.is_retryable());
998 assert!(!NoxuError::environment("x").is_retryable());
999 assert!(!NoxuError::DatabaseClosed.is_retryable());
1000 }
1001
1002 #[test]
1003 fn test_is_fatal_to_environment() {
1004 assert!(
1005 NoxuError::environment_with_reason(
1006 EnvironmentFailureReason::LogChecksum,
1007 "x"
1008 )
1009 .is_fatal_to_environment()
1010 );
1011 assert!(
1012 NoxuError::LogChecksumMismatch("bad".into())
1013 .is_fatal_to_environment()
1014 );
1015 assert!(
1016 NoxuError::LogWriteFailure("io".into()).is_fatal_to_environment()
1017 );
1018 assert!(
1019 NoxuError::DiskLimitExceeded { used: 100, limit: 50 }
1020 .is_fatal_to_environment()
1021 );
1022 assert!(
1023 NoxuError::EnvironmentWedged("x".into()).is_fatal_to_environment()
1024 );
1025
1026 assert!(
1028 !NoxuError::environment_with_reason(
1029 EnvironmentFailureReason::UnexpectedState,
1030 "x"
1031 )
1032 .is_fatal_to_environment()
1033 );
1034
1035 assert!(!NoxuError::DeadlockDetected.is_fatal_to_environment());
1036 assert!(!NoxuError::NotFound.is_fatal_to_environment());
1037 assert!(!NoxuError::LockConflict("x".into()).is_fatal_to_environment());
1038 }
1039
1040 #[test]
1041 fn test_new_variants() {
1042 let e =
1043 NoxuError::LockTimeout { timeout_ms: 250, detail: String::new() };
1044 assert!(e.to_string().contains("250ms"));
1045
1046 let e = NoxuError::TransactionTimeout { timeout_ms: 1000, txn_id: 7 };
1047 assert!(e.to_string().contains("1000ms"));
1048 assert!(e.to_string().contains("7"));
1049
1050 let e = NoxuError::LockPreempted;
1051 assert!(e.to_string().contains("preempted"));
1052
1053 let e = NoxuError::LockNotAvailable;
1054 assert!(e.to_string().contains("no-wait"));
1055
1056 let e = NoxuError::UniqueConstraintViolation("idx_email".into());
1057 assert!(e.to_string().contains("unique constraint"));
1058
1059 let e = NoxuError::DeleteConstraintViolation("key=42".into());
1060 assert!(e.to_string().contains("delete constraint"));
1061
1062 let e = NoxuError::ForeignConstraintViolation("fk_user".into());
1063 assert!(e.to_string().contains("foreign constraint"));
1064
1065 let e = NoxuError::DuplicateDataException;
1066 assert!(e.to_string().contains("duplicate data"));
1067
1068 let e = NoxuError::ReplicaWrite;
1069 assert!(e.to_string().contains("replica"));
1070
1071 let e = NoxuError::InsufficientReplicas { required: 3, available: 1 };
1072 assert!(e.to_string().contains("required 3"));
1073 assert!(e.to_string().contains("available 1"));
1074
1075 let e = NoxuError::RollbackRequired("ha failover".into());
1076 assert!(e.to_string().contains("rollback required"));
1077
1078 let e = NoxuError::LogChecksumMismatch("file 7".into());
1079 assert!(e.to_string().contains("log checksum mismatch"));
1080
1081 let e = NoxuError::LogFileNotFound("00000007.ndb".into());
1082 assert!(e.to_string().contains("log file not found"));
1083
1084 let e = NoxuError::EnvironmentWedged("perm fail".into());
1085 assert!(e.to_string().contains("permanent failure"));
1086
1087 let e = NoxuError::EnvironmentNotFound("/bad/path".into());
1088 assert!(e.to_string().contains("not found"));
1089
1090 let e = NoxuError::EnvironmentLocked("/data".into());
1091 assert!(e.to_string().contains("locked"));
1092
1093 let e = NoxuError::LogWriteFailure("ENOSPC".into());
1094 assert!(e.to_string().contains("log write failure"));
1095
1096 let e = NoxuError::DiskLimitExceeded { used: 1000, limit: 500 };
1097 assert!(e.to_string().contains("used=1000"));
1098 assert!(e.to_string().contains("limit=500"));
1099
1100 let e = NoxuError::ThreadInterrupted;
1101 assert!(e.to_string().contains("interrupted"));
1102 }
1103
1104 #[test]
1105 fn test_sequence_variants() {
1106 let e = NoxuError::SequenceExists("counter".into());
1107 assert!(e.to_string().contains("counter"));
1108
1109 let e = NoxuError::SequenceNotFound("counter".into());
1110 assert!(e.to_string().contains("sequence not found"));
1111
1112 let e = NoxuError::SequenceOverflow;
1113 assert!(e.to_string().contains("overflow"));
1114
1115 let e = NoxuError::SequenceIntegrity("bad state".into());
1116 assert!(e.to_string().contains("integrity"));
1117 }
1118
1119 #[test]
1120 fn test_database_already_exists() {
1121 let e = NoxuError::DatabaseAlreadyExists("mydb".into());
1122 assert!(e.to_string().contains("mydb"));
1123 }
1124
1125 #[test]
1126 fn test_lock_conflict() {
1127 let e = NoxuError::LockConflict("timeout".into());
1128 assert!(e.to_string().contains("lock conflict"));
1129 }
1130
1131 #[test]
1132 fn test_transaction_aborted() {
1133 let e = NoxuError::TransactionAborted("rolled back".into());
1134 assert!(e.to_string().contains("transaction aborted"));
1135 }
1136
1137 #[test]
1138 fn test_helpers() {
1139 assert!(matches!(
1140 NoxuError::environment("x"),
1141 NoxuError::EnvironmentFailure { .. }
1142 ));
1143 assert!(matches!(
1144 NoxuError::database("x"),
1145 NoxuError::OperationNotAllowed(_)
1146 ));
1147 assert!(matches!(
1148 NoxuError::invalid_argument("x"),
1149 NoxuError::IllegalArgument(_)
1150 ));
1151 assert!(matches!(
1152 NoxuError::lock_conflict("x"),
1153 NoxuError::LockConflict(_)
1154 ));
1155 assert!(matches!(
1156 NoxuError::lock_timeout(500),
1157 NoxuError::LockTimeout { timeout_ms: 500, .. }
1158 ));
1159 assert!(matches!(
1160 NoxuError::database_not_found("db"),
1161 NoxuError::DatabaseNotFound(_)
1162 ));
1163 assert!(matches!(
1164 NoxuError::disk_limit_exceeded(100, 50),
1165 NoxuError::DiskLimitExceeded { used: 100, limit: 50 }
1166 ));
1167 }
1168
1169 #[test]
1170 fn test_is_lock_conflict() {
1171 assert!(NoxuError::LockConflict("x".into()).is_lock_conflict());
1172 assert!(NoxuError::DeadlockDetected.is_lock_conflict());
1173 assert!(NoxuError::LockPreempted.is_lock_conflict());
1174 assert!(NoxuError::LockNotAvailable.is_lock_conflict());
1175 assert!(
1176 !NoxuError::LockTimeout { timeout_ms: 500, detail: String::new() }
1177 .is_lock_conflict()
1178 );
1179 assert!(!NoxuError::NotFound.is_lock_conflict());
1180 }
1181
1182 #[test]
1183 fn test_is_lock_timeout() {
1184 assert!(
1185 NoxuError::LockTimeout { timeout_ms: 500, detail: String::new() }
1186 .is_lock_timeout()
1187 );
1188 assert!(
1189 NoxuError::TransactionTimeout { timeout_ms: 1000, txn_id: 1 }
1190 .is_lock_timeout()
1191 );
1192 assert!(!NoxuError::LockConflict("x".into()).is_lock_timeout());
1193 assert!(!NoxuError::NotFound.is_lock_timeout());
1194 }
1195
1196 #[test]
1197 fn test_is_database_not_found() {
1198 assert!(
1199 NoxuError::DatabaseNotFound("mydb".into()).is_database_not_found()
1200 );
1201 assert!(!NoxuError::DatabaseClosed.is_database_not_found());
1202 assert!(!NoxuError::NotFound.is_database_not_found());
1203 }
1204
1205 #[test]
1206 fn test_exception_event() {
1207 let evt = ExceptionEvent::new(
1208 "cleaner failed",
1209 ExceptionSource::Cleaner,
1210 "cleaner-1",
1211 );
1212 assert_eq!(evt.message, "cleaner failed");
1213 assert_eq!(evt.source, ExceptionSource::Cleaner);
1214 assert_eq!(evt.thread_name, "cleaner-1");
1215 assert!(evt.source.to_string().contains("Cleaner"));
1216 }
1217
1218 #[test]
1219 fn test_exception_listener_trait() {
1220 struct NoopListener;
1221 impl ExceptionListener for NoopListener {
1222 fn exception_event(&self, _event: &ExceptionEvent) {}
1223 }
1224 let listener: Box<dyn ExceptionListener> = Box::new(NoopListener);
1225 let evt =
1226 ExceptionEvent::new("x", ExceptionSource::Checkpointer, "ckpt");
1227 listener.exception_event(&evt);
1228 }
1229
1230 #[test]
1231 fn test_reason_display() {
1232 assert_eq!(
1233 EnvironmentFailureReason::LogChecksum.to_string(),
1234 "LOG_CHECKSUM"
1235 );
1236 assert_eq!(
1237 EnvironmentFailureReason::BtreeCorruption.to_string(),
1238 "BTREE_CORRUPTION"
1239 );
1240 assert_eq!(
1241 EnvironmentFailureReason::DiskLimit.to_string(),
1242 "DISK_LIMIT"
1243 );
1244 assert_eq!(
1245 EnvironmentFailureReason::Other("CUSTOM".into()).to_string(),
1246 "CUSTOM"
1247 );
1248 }
1249}