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