nodedb_types/error/
types.rs1use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use super::code::ErrorCode;
8use super::details::ErrorDetails;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct NodeDbError {
16 pub(super) code: ErrorCode,
17 pub(super) message: String,
18 pub(super) details: ErrorDetails,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub(super) cause: Option<Box<NodeDbError>>,
21}
22
23impl fmt::Display for NodeDbError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 write!(f, "[{}] {}", self.code, self.message)
26 }
27}
28
29impl std::error::Error for NodeDbError {
30 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
31 self.cause.as_deref().map(|e| e as &dyn std::error::Error)
32 }
33}
34
35impl NodeDbError {
38 pub fn code(&self) -> ErrorCode {
40 self.code
41 }
42
43 pub fn message(&self) -> &str {
45 &self.message
46 }
47
48 pub fn details(&self) -> &ErrorDetails {
50 &self.details
51 }
52
53 pub fn cause(&self) -> Option<&NodeDbError> {
55 self.cause.as_deref()
56 }
57
58 pub fn with_cause(mut self, cause: NodeDbError) -> Self {
60 self.cause = Some(Box::new(cause));
61 self
62 }
63
64 pub fn is_retriable(&self) -> bool {
66 matches!(
67 self.details,
68 ErrorDetails::WriteConflict { .. }
69 | ErrorDetails::DeadlineExceeded
70 | ErrorDetails::NoLeader
71 | ErrorDetails::NotLeader { .. }
72 | ErrorDetails::MigrationInProgress
73 | ErrorDetails::NodeUnreachable
74 | ErrorDetails::SyncConnectionFailed
75 | ErrorDetails::Bridge
76 | ErrorDetails::MemoryExhausted { .. }
77 )
78 }
79
80 pub fn is_client_error(&self) -> bool {
82 matches!(
83 self.details,
84 ErrorDetails::BadRequest
85 | ErrorDetails::ConstraintViolation { .. }
86 | ErrorDetails::AppendOnlyViolation { .. }
87 | ErrorDetails::BalanceViolation { .. }
88 | ErrorDetails::PeriodLocked { .. }
89 | ErrorDetails::StateTransitionViolation { .. }
90 | ErrorDetails::TransitionCheckViolation { .. }
91 | ErrorDetails::RetentionViolation { .. }
92 | ErrorDetails::LegalHoldActive { .. }
93 | ErrorDetails::CollectionNotFound { .. }
94 | ErrorDetails::DocumentNotFound { .. }
95 | ErrorDetails::AuthorizationDenied { .. }
96 | ErrorDetails::AuthExpired
97 | ErrorDetails::Config
98 | ErrorDetails::SqlNotEnabled
99 )
100 }
101}
102
103impl NodeDbError {
106 pub fn is_constraint_violation(&self) -> bool {
107 matches!(self.details, ErrorDetails::ConstraintViolation { .. })
108 }
109 pub fn is_not_found(&self) -> bool {
110 matches!(
111 self.details,
112 ErrorDetails::CollectionNotFound { .. } | ErrorDetails::DocumentNotFound { .. }
113 )
114 }
115 pub fn is_auth_denied(&self) -> bool {
116 matches!(self.details, ErrorDetails::AuthorizationDenied { .. })
117 }
118 pub fn is_storage(&self) -> bool {
119 matches!(
120 self.details,
121 ErrorDetails::Storage
122 | ErrorDetails::SegmentCorrupted
123 | ErrorDetails::ColdStorage
124 | ErrorDetails::Wal
125 )
126 }
127 pub fn is_internal(&self) -> bool {
128 matches!(self.details, ErrorDetails::Internal)
129 }
130 pub fn is_type_mismatch(&self) -> bool {
131 matches!(self.details, ErrorDetails::TypeMismatch { .. })
132 }
133 pub fn is_type_guard_violation(&self) -> bool {
134 matches!(self.details, ErrorDetails::TypeGuardViolation { .. })
135 }
136 pub fn is_overflow(&self) -> bool {
137 matches!(self.details, ErrorDetails::Overflow { .. })
138 }
139 pub fn is_insufficient_balance(&self) -> bool {
140 matches!(self.details, ErrorDetails::InsufficientBalance { .. })
141 }
142 pub fn is_rate_exceeded(&self) -> bool {
143 matches!(self.details, ErrorDetails::RateExceeded { .. })
144 }
145 pub fn is_cluster(&self) -> bool {
146 matches!(
147 self.details,
148 ErrorDetails::NoLeader
149 | ErrorDetails::NotLeader { .. }
150 | ErrorDetails::MigrationInProgress
151 | ErrorDetails::NodeUnreachable
152 | ErrorDetails::Cluster
153 )
154 }
155}
156
157pub type NodeDbResult<T> = std::result::Result<T, NodeDbError>;
159
160impl From<std::io::Error> for NodeDbError {
161 fn from(e: std::io::Error) -> Self {
162 Self::storage(e)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn error_display_includes_code() {
172 let e = NodeDbError::constraint_violation("users", "duplicate email");
173 let msg = e.to_string();
174 assert!(msg.contains("NDB-1000"));
175 assert!(msg.contains("constraint violation"));
176 assert!(msg.contains("users"));
177 }
178
179 #[test]
180 fn error_code_accessor() {
181 let e = NodeDbError::write_conflict("orders", "abc");
182 assert_eq!(e.code(), ErrorCode::WRITE_CONFLICT);
183 assert_eq!(e.code().0, 1001);
184 }
185
186 #[test]
187 fn details_matching() {
188 let e = NodeDbError::collection_not_found("users");
189 assert!(matches!(
190 e.details(),
191 ErrorDetails::CollectionNotFound { collection } if collection == "users"
192 ));
193 assert!(e.is_not_found());
194 }
195
196 #[test]
197 fn error_cause_chaining() {
198 let inner = NodeDbError::storage("disk full");
199 let outer = NodeDbError::internal("write failed").with_cause(inner);
200 assert!(outer.cause().is_some());
201 assert!(outer.cause().unwrap().is_storage());
202 assert!(outer.cause().unwrap().message().contains("disk full"));
203 }
204
205 #[test]
206 fn io_error_converts() {
207 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
208 let e: NodeDbError = io_err.into();
209 assert!(e.is_storage());
210 assert_eq!(e.code(), ErrorCode::STORAGE);
211 }
212
213 #[test]
214 fn retriable_errors() {
215 assert!(NodeDbError::write_conflict("x", "y").is_retriable());
216 assert!(NodeDbError::deadline_exceeded().is_retriable());
217 assert!(!NodeDbError::bad_request("bad").is_retriable());
218 }
219
220 #[test]
221 fn client_errors() {
222 assert!(NodeDbError::bad_request("bad").is_client_error());
223 assert!(!NodeDbError::internal("oops").is_client_error());
224 }
225
226 #[test]
227 fn json_serialization() {
228 let e = NodeDbError::collection_not_found("users");
229 let json = serde_json::to_value(&e).unwrap();
230 assert_eq!(json["code"], 1100);
231 assert!(json["message"].as_str().unwrap().contains("users"));
232 assert_eq!(json["details"]["kind"], "collection_not_found");
233 assert_eq!(json["details"]["collection"], "users");
234 }
235
236 #[test]
237 fn sync_delta_rejected_ctor() {
238 let e = NodeDbError::sync_delta_rejected(
239 "unique violation",
240 Some(
241 crate::sync::compensation::CompensationHint::UniqueViolation {
242 field: "email".into(),
243 conflicting_value: "a@b.com".into(),
244 },
245 ),
246 );
247 assert!(e.to_string().contains("NDB-3001"));
248 assert!(e.to_string().contains("sync delta rejected"));
249 }
250
251 #[test]
252 fn sql_not_enabled_ctor() {
253 let e = NodeDbError::sql_not_enabled();
254 assert!(e.to_string().contains("SQL not enabled"));
255 assert_eq!(e.code(), ErrorCode::SQL_NOT_ENABLED);
256 }
257}