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::TransactionRollback { .. }
72 | ErrorDetails::DeadlineExceeded
73 | ErrorDetails::NoLeader
74 | ErrorDetails::NotLeader { .. }
75 | ErrorDetails::MigrationInProgress
76 | ErrorDetails::NodeUnreachable
77 | ErrorDetails::SyncConnectionFailed
78 | ErrorDetails::Bridge { .. }
79 | ErrorDetails::MemoryExhausted { .. }
80 )
81 }
82
83 pub fn is_client_error(&self) -> bool {
85 matches!(
86 self.details,
87 ErrorDetails::BadRequest
88 | ErrorDetails::ConstraintViolation { .. }
89 | ErrorDetails::AppendOnlyViolation { .. }
90 | ErrorDetails::BalanceViolation { .. }
91 | ErrorDetails::PeriodLocked { .. }
92 | ErrorDetails::StateTransitionViolation { .. }
93 | ErrorDetails::TransitionCheckViolation { .. }
94 | ErrorDetails::RetentionViolation { .. }
95 | ErrorDetails::LegalHoldActive { .. }
96 | ErrorDetails::CollectionNotFound { .. }
97 | ErrorDetails::DocumentNotFound { .. }
98 | ErrorDetails::AuthorizationDenied { .. }
99 | ErrorDetails::AuthExpired
100 | ErrorDetails::AuthenticationFailed
101 | ErrorDetails::Config
102 | ErrorDetails::SqlNotEnabled
103 | ErrorDetails::UndefinedFunction { .. }
104 | ErrorDetails::UndefinedColumn { .. }
105 | ErrorDetails::AmbiguousColumn { .. }
106 | ErrorDetails::DivisionByZero
107 | ErrorDetails::DataException { .. }
108 | ErrorDetails::ProgramLimitExceeded { .. }
109 | ErrorDetails::ActiveSqlTransaction { .. }
110 | ErrorDetails::DependentObjectsExist { .. }
111 | ErrorDetails::InvalidLimitValue { .. }
112 | ErrorDetails::BackupTenantMismatch { .. }
113 | ErrorDetails::BackupKeyMismatch
114 )
115 }
116}
117
118impl NodeDbError {
121 pub fn is_constraint_violation(&self) -> bool {
122 matches!(self.details, ErrorDetails::ConstraintViolation { .. })
123 }
124 pub fn is_not_found(&self) -> bool {
125 matches!(
126 self.details,
127 ErrorDetails::CollectionNotFound { .. }
128 | ErrorDetails::DocumentNotFound { .. }
129 | ErrorDetails::DatabaseNotFound { .. }
130 )
131 }
132 pub fn is_auth_denied(&self) -> bool {
133 matches!(self.details, ErrorDetails::AuthorizationDenied { .. })
134 }
135 pub fn is_storage(&self) -> bool {
136 matches!(
137 self.details,
138 ErrorDetails::Storage { .. }
139 | ErrorDetails::SegmentCorrupted { .. }
140 | ErrorDetails::ColdStorage { .. }
141 | ErrorDetails::Wal { .. }
142 )
143 }
144 pub fn is_internal(&self) -> bool {
145 matches!(self.details, ErrorDetails::Internal { .. })
146 }
147 pub fn is_type_mismatch(&self) -> bool {
148 matches!(self.details, ErrorDetails::TypeMismatch { .. })
149 }
150 pub fn is_type_guard_violation(&self) -> bool {
151 matches!(self.details, ErrorDetails::TypeGuardViolation { .. })
152 }
153 pub fn is_overflow(&self) -> bool {
154 matches!(self.details, ErrorDetails::Overflow { .. })
155 }
156 pub fn is_insufficient_balance(&self) -> bool {
157 matches!(self.details, ErrorDetails::InsufficientBalance { .. })
158 }
159 pub fn is_rate_exceeded(&self) -> bool {
160 matches!(self.details, ErrorDetails::RateExceeded { .. })
161 }
162 pub fn is_cluster(&self) -> bool {
163 matches!(
164 self.details,
165 ErrorDetails::NoLeader
166 | ErrorDetails::NotLeader { .. }
167 | ErrorDetails::MigrationInProgress
168 | ErrorDetails::NodeUnreachable
169 | ErrorDetails::Cluster
170 )
171 }
172}
173
174pub type NodeDbResult<T> = std::result::Result<T, NodeDbError>;
176
177impl From<std::io::Error> for NodeDbError {
178 fn from(e: std::io::Error) -> Self {
179 Self::storage(e)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn error_display_includes_code() {
189 let e = NodeDbError::constraint_violation("users", "unique", "duplicate email");
190 let msg = e.to_string();
191 assert!(msg.contains("NDB-1000"));
192 assert!(msg.contains("constraint violation"));
193 assert!(msg.contains("users"));
194 }
195
196 #[test]
197 fn error_code_accessor() {
198 let e = NodeDbError::write_conflict("orders", "abc");
199 assert_eq!(e.code(), ErrorCode::WRITE_CONFLICT);
200 assert_eq!(e.code().0, 1001);
201 }
202
203 #[test]
204 fn details_matching() {
205 let e = NodeDbError::collection_not_found("users");
206 assert!(matches!(
207 e.details(),
208 ErrorDetails::CollectionNotFound { collection } if collection == "users"
209 ));
210 assert!(e.is_not_found());
211 }
212
213 #[test]
214 fn error_cause_chaining() {
215 let inner = NodeDbError::storage("disk full");
216 let outer = NodeDbError::internal("write failed").with_cause(inner);
217 assert!(outer.cause().is_some());
218 assert!(outer.cause().unwrap().is_storage());
219 assert!(outer.cause().unwrap().message().contains("disk full"));
220 }
221
222 #[test]
223 fn io_error_converts() {
224 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
225 let e: NodeDbError = io_err.into();
226 assert!(e.is_storage());
227 assert_eq!(e.code(), ErrorCode::STORAGE);
228 }
229
230 #[test]
231 fn retriable_errors() {
232 assert!(NodeDbError::write_conflict("x", "y").is_retriable());
233 assert!(NodeDbError::deadline_exceeded().is_retriable());
234 assert!(!NodeDbError::bad_request("bad").is_retriable());
235 }
236
237 #[test]
238 fn client_errors() {
239 assert!(NodeDbError::bad_request("bad").is_client_error());
240 assert!(!NodeDbError::internal("oops").is_client_error());
241 }
242
243 #[test]
246 fn transaction_and_dependency_codes_classify() {
247 let rollback = NodeDbError::transaction_rollback("participant failed");
248 assert!(rollback.is_retriable());
249 assert!(!rollback.is_client_error());
250 assert_eq!(rollback.code(), ErrorCode::TRANSACTION_ROLLBACK);
251
252 let in_block = NodeDbError::active_sql_transaction("VACUUM");
253 assert!(in_block.is_client_error());
254 assert!(!in_block.is_retriable());
255 assert_eq!(in_block.code(), ErrorCode::ACTIVE_SQL_TRANSACTION);
256
257 let dependents = NodeDbError::dependent_objects_exist("role \"r\"", "held by users");
258 assert!(dependents.is_client_error());
259 assert!(!dependents.is_retriable());
260 assert_eq!(dependents.code(), ErrorCode::DEPENDENT_OBJECTS_EXIST);
261 }
262
263 #[test]
264 fn json_serialization() {
265 let e = NodeDbError::collection_not_found("users");
266 let json = serde_json::to_value(&e).unwrap();
267 assert_eq!(json["code"], 1100);
268 assert!(json["message"].as_str().unwrap().contains("users"));
269 assert_eq!(json["details"]["kind"], "collection_not_found");
270 assert_eq!(json["details"]["collection"], "users");
271 }
272
273 #[test]
274 fn sync_delta_rejected_ctor() {
275 let e = NodeDbError::sync_delta_rejected(
276 "unique violation",
277 Some(
278 crate::sync::compensation::CompensationHint::UniqueViolation {
279 field: "email".into(),
280 conflicting_value: "a@b.com".into(),
281 },
282 ),
283 );
284 assert!(e.to_string().contains("NDB-3001"));
285 assert!(e.to_string().contains("sync delta rejected"));
286 }
287
288 #[test]
289 fn sql_not_enabled_ctor() {
290 let e = NodeDbError::sql_not_enabled();
291 assert!(e.to_string().contains("SQL not enabled"));
292 assert_eq!(e.code(), ErrorCode::SQL_NOT_ENABLED);
293 }
294}