1use std::fmt;
39
40use serde::{Deserialize, Serialize};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub struct ErrorCode(pub u16);
49
50impl ErrorCode {
51 pub const CONSTRAINT_VIOLATION: Self = Self(1000);
53 pub const WRITE_CONFLICT: Self = Self(1001);
54 pub const DEADLINE_EXCEEDED: Self = Self(1002);
55 pub const PREVALIDATION_REJECTED: Self = Self(1003);
56
57 pub const COLLECTION_NOT_FOUND: Self = Self(1100);
59 pub const DOCUMENT_NOT_FOUND: Self = Self(1101);
60
61 pub const PLAN_ERROR: Self = Self(1200);
63 pub const FAN_OUT_EXCEEDED: Self = Self(1201);
64 pub const SQL_NOT_ENABLED: Self = Self(1202);
65
66 pub const AUTHORIZATION_DENIED: Self = Self(2000);
68 pub const AUTH_EXPIRED: Self = Self(2001);
69
70 pub const SYNC_CONNECTION_FAILED: Self = Self(3000);
72 pub const SYNC_DELTA_REJECTED: Self = Self(3001);
73 pub const SHAPE_SUBSCRIPTION_FAILED: Self = Self(3002);
74
75 pub const STORAGE: Self = Self(4000);
77 pub const SEGMENT_CORRUPTED: Self = Self(4001);
78 pub const COLD_STORAGE: Self = Self(4002);
79
80 pub const WAL: Self = Self(4100);
82
83 pub const SERIALIZATION: Self = Self(4200);
85 pub const CODEC: Self = Self(4201);
86
87 pub const CONFIG: Self = Self(5000);
89 pub const BAD_REQUEST: Self = Self(5001);
90
91 pub const NO_LEADER: Self = Self(6000);
93 pub const NOT_LEADER: Self = Self(6001);
94 pub const MIGRATION_IN_PROGRESS: Self = Self(6002);
95 pub const NODE_UNREACHABLE: Self = Self(6003);
96 pub const CLUSTER: Self = Self(6010);
97
98 pub const MEMORY_EXHAUSTED: Self = Self(7000);
100
101 pub const ENCRYPTION: Self = Self(8000);
103
104 pub const INTERNAL: Self = Self(9000);
106 pub const BRIDGE: Self = Self(9001);
107 pub const DISPATCH: Self = Self(9002);
108}
109
110impl fmt::Display for ErrorCode {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 write!(f, "NDB-{:04}", self.0)
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(tag = "kind", rename_all = "snake_case")]
127pub enum ErrorDetails {
128 ConstraintViolation {
130 collection: String,
131 },
132 WriteConflict {
133 collection: String,
134 document_id: String,
135 },
136 DeadlineExceeded,
137 PrevalidationRejected {
138 constraint: String,
139 },
140
141 CollectionNotFound {
143 collection: String,
144 },
145 DocumentNotFound {
146 collection: String,
147 document_id: String,
148 },
149
150 PlanError,
152 FanOutExceeded {
153 shards_touched: u16,
154 limit: u16,
155 },
156 SqlNotEnabled,
157
158 AuthorizationDenied {
160 resource: String,
161 },
162 AuthExpired,
163
164 SyncConnectionFailed,
166 SyncDeltaRejected {
167 compensation: Option<crate::sync::compensation::CompensationHint>,
168 },
169 ShapeSubscriptionFailed {
170 shape_id: String,
171 },
172
173 Storage,
175 SegmentCorrupted,
176 ColdStorage,
177 Wal,
178
179 Serialization {
181 format: String,
182 },
183 Codec,
184
185 Config,
187 BadRequest,
188
189 NoLeader,
191 NotLeader {
192 leader_addr: String,
193 },
194 MigrationInProgress,
195 NodeUnreachable,
196 Cluster,
197
198 MemoryExhausted {
200 engine: String,
201 },
202
203 Encryption,
205
206 Bridge,
208 Dispatch,
209 Internal,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct NodeDbError {
222 code: ErrorCode,
223 message: String,
224 details: ErrorDetails,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 cause: Option<Box<NodeDbError>>,
227}
228
229impl fmt::Display for NodeDbError {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 write!(f, "[{}] {}", self.code, self.message)
232 }
233}
234
235impl std::error::Error for NodeDbError {
236 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
237 self.cause.as_deref().map(|e| e as &dyn std::error::Error)
238 }
239}
240
241impl NodeDbError {
246 pub fn code(&self) -> ErrorCode {
248 self.code
249 }
250
251 pub fn message(&self) -> &str {
253 &self.message
254 }
255
256 pub fn details(&self) -> &ErrorDetails {
258 &self.details
259 }
260
261 pub fn cause(&self) -> Option<&NodeDbError> {
263 self.cause.as_deref()
264 }
265
266 pub fn with_cause(mut self, cause: NodeDbError) -> Self {
268 self.cause = Some(Box::new(cause));
269 self
270 }
271
272 pub fn is_retriable(&self) -> bool {
274 matches!(
275 self.details,
276 ErrorDetails::WriteConflict { .. }
277 | ErrorDetails::DeadlineExceeded
278 | ErrorDetails::NoLeader
279 | ErrorDetails::NotLeader { .. }
280 | ErrorDetails::MigrationInProgress
281 | ErrorDetails::NodeUnreachable
282 | ErrorDetails::SyncConnectionFailed
283 | ErrorDetails::Bridge
284 | ErrorDetails::MemoryExhausted { .. }
285 )
286 }
287
288 pub fn is_client_error(&self) -> bool {
290 matches!(
291 self.details,
292 ErrorDetails::BadRequest
293 | ErrorDetails::ConstraintViolation { .. }
294 | ErrorDetails::CollectionNotFound { .. }
295 | ErrorDetails::DocumentNotFound { .. }
296 | ErrorDetails::AuthorizationDenied { .. }
297 | ErrorDetails::AuthExpired
298 | ErrorDetails::Config
299 | ErrorDetails::SqlNotEnabled
300 )
301 }
302}
303
304impl NodeDbError {
309 pub fn is_constraint_violation(&self) -> bool {
310 matches!(self.details, ErrorDetails::ConstraintViolation { .. })
311 }
312 pub fn is_not_found(&self) -> bool {
313 matches!(
314 self.details,
315 ErrorDetails::CollectionNotFound { .. } | ErrorDetails::DocumentNotFound { .. }
316 )
317 }
318 pub fn is_auth_denied(&self) -> bool {
319 matches!(self.details, ErrorDetails::AuthorizationDenied { .. })
320 }
321 pub fn is_storage(&self) -> bool {
322 matches!(
323 self.details,
324 ErrorDetails::Storage
325 | ErrorDetails::SegmentCorrupted
326 | ErrorDetails::ColdStorage
327 | ErrorDetails::Wal
328 )
329 }
330 pub fn is_internal(&self) -> bool {
331 matches!(self.details, ErrorDetails::Internal)
332 }
333 pub fn is_cluster(&self) -> bool {
334 matches!(
335 self.details,
336 ErrorDetails::NoLeader
337 | ErrorDetails::NotLeader { .. }
338 | ErrorDetails::MigrationInProgress
339 | ErrorDetails::NodeUnreachable
340 | ErrorDetails::Cluster
341 )
342 }
343}
344
345impl NodeDbError {
350 pub fn constraint_violation(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
353 let collection = collection.into();
354 Self {
355 code: ErrorCode::CONSTRAINT_VIOLATION,
356 message: format!("constraint violation on {collection}: {detail}"),
357 details: ErrorDetails::ConstraintViolation { collection },
358 cause: None,
359 }
360 }
361
362 pub fn write_conflict(collection: impl Into<String>, document_id: impl Into<String>) -> Self {
363 let collection = collection.into();
364 let document_id = document_id.into();
365 Self {
366 code: ErrorCode::WRITE_CONFLICT,
367 message: format!(
368 "write conflict on {collection}/{document_id}, retry with idempotency key"
369 ),
370 details: ErrorDetails::WriteConflict {
371 collection,
372 document_id,
373 },
374 cause: None,
375 }
376 }
377
378 pub fn deadline_exceeded() -> Self {
379 Self {
380 code: ErrorCode::DEADLINE_EXCEEDED,
381 message: "request exceeded deadline".into(),
382 details: ErrorDetails::DeadlineExceeded,
383 cause: None,
384 }
385 }
386
387 pub fn prevalidation_rejected(
388 constraint: impl Into<String>,
389 reason: impl fmt::Display,
390 ) -> Self {
391 let constraint = constraint.into();
392 Self {
393 code: ErrorCode::PREVALIDATION_REJECTED,
394 message: format!("pre-validation rejected: {constraint} — {reason}"),
395 details: ErrorDetails::PrevalidationRejected { constraint },
396 cause: None,
397 }
398 }
399
400 pub fn collection_not_found(collection: impl Into<String>) -> Self {
403 let collection = collection.into();
404 Self {
405 code: ErrorCode::COLLECTION_NOT_FOUND,
406 message: format!("collection '{collection}' not found"),
407 details: ErrorDetails::CollectionNotFound { collection },
408 cause: None,
409 }
410 }
411
412 pub fn document_not_found(
413 collection: impl Into<String>,
414 document_id: impl Into<String>,
415 ) -> Self {
416 let collection = collection.into();
417 let document_id = document_id.into();
418 Self {
419 code: ErrorCode::DOCUMENT_NOT_FOUND,
420 message: format!("document '{document_id}' not found in '{collection}'"),
421 details: ErrorDetails::DocumentNotFound {
422 collection,
423 document_id,
424 },
425 cause: None,
426 }
427 }
428
429 pub fn plan_error(detail: impl fmt::Display) -> Self {
432 Self {
433 code: ErrorCode::PLAN_ERROR,
434 message: format!("query plan error: {detail}"),
435 details: ErrorDetails::PlanError,
436 cause: None,
437 }
438 }
439
440 pub fn fan_out_exceeded(shards_touched: u16, limit: u16) -> Self {
441 Self {
442 code: ErrorCode::FAN_OUT_EXCEEDED,
443 message: format!("query fan-out exceeded: {shards_touched} shards > limit {limit}"),
444 details: ErrorDetails::FanOutExceeded {
445 shards_touched,
446 limit,
447 },
448 cause: None,
449 }
450 }
451
452 pub fn sql_not_enabled() -> Self {
453 Self {
454 code: ErrorCode::SQL_NOT_ENABLED,
455 message: "SQL not enabled (compile with 'sql' feature)".into(),
456 details: ErrorDetails::SqlNotEnabled,
457 cause: None,
458 }
459 }
460
461 pub fn authorization_denied(resource: impl Into<String>) -> Self {
464 let resource = resource.into();
465 Self {
466 code: ErrorCode::AUTHORIZATION_DENIED,
467 message: format!("authorization denied on {resource}"),
468 details: ErrorDetails::AuthorizationDenied { resource },
469 cause: None,
470 }
471 }
472
473 pub fn auth_expired(detail: impl fmt::Display) -> Self {
474 Self {
475 code: ErrorCode::AUTH_EXPIRED,
476 message: format!("auth expired: {detail}"),
477 details: ErrorDetails::AuthExpired,
478 cause: None,
479 }
480 }
481
482 pub fn sync_connection_failed(detail: impl fmt::Display) -> Self {
485 Self {
486 code: ErrorCode::SYNC_CONNECTION_FAILED,
487 message: format!("sync connection failed: {detail}"),
488 details: ErrorDetails::SyncConnectionFailed,
489 cause: None,
490 }
491 }
492
493 pub fn sync_delta_rejected(
494 reason: impl fmt::Display,
495 compensation: Option<crate::sync::compensation::CompensationHint>,
496 ) -> Self {
497 Self {
498 code: ErrorCode::SYNC_DELTA_REJECTED,
499 message: format!("sync delta rejected: {reason}"),
500 details: ErrorDetails::SyncDeltaRejected { compensation },
501 cause: None,
502 }
503 }
504
505 pub fn shape_subscription_failed(
506 shape_id: impl Into<String>,
507 detail: impl fmt::Display,
508 ) -> Self {
509 let shape_id = shape_id.into();
510 Self {
511 code: ErrorCode::SHAPE_SUBSCRIPTION_FAILED,
512 message: format!("shape subscription failed for '{shape_id}': {detail}"),
513 details: ErrorDetails::ShapeSubscriptionFailed { shape_id },
514 cause: None,
515 }
516 }
517
518 pub fn storage(detail: impl fmt::Display) -> Self {
521 Self {
522 code: ErrorCode::STORAGE,
523 message: format!("storage error: {detail}"),
524 details: ErrorDetails::Storage,
525 cause: None,
526 }
527 }
528
529 pub fn segment_corrupted(detail: impl fmt::Display) -> Self {
530 Self {
531 code: ErrorCode::SEGMENT_CORRUPTED,
532 message: format!("segment corrupted: {detail}"),
533 details: ErrorDetails::SegmentCorrupted,
534 cause: None,
535 }
536 }
537
538 pub fn cold_storage(detail: impl fmt::Display) -> Self {
539 Self {
540 code: ErrorCode::COLD_STORAGE,
541 message: format!("cold storage error: {detail}"),
542 details: ErrorDetails::ColdStorage,
543 cause: None,
544 }
545 }
546
547 pub fn wal(detail: impl fmt::Display) -> Self {
548 Self {
549 code: ErrorCode::WAL,
550 message: format!("WAL error: {detail}"),
551 details: ErrorDetails::Wal,
552 cause: None,
553 }
554 }
555
556 pub fn serialization(format: impl Into<String>, detail: impl fmt::Display) -> Self {
559 let format = format.into();
560 Self {
561 code: ErrorCode::SERIALIZATION,
562 message: format!("serialization error ({format}): {detail}"),
563 details: ErrorDetails::Serialization { format },
564 cause: None,
565 }
566 }
567
568 pub fn codec(detail: impl fmt::Display) -> Self {
569 Self {
570 code: ErrorCode::CODEC,
571 message: format!("codec error: {detail}"),
572 details: ErrorDetails::Codec,
573 cause: None,
574 }
575 }
576
577 pub fn config(detail: impl fmt::Display) -> Self {
580 Self {
581 code: ErrorCode::CONFIG,
582 message: format!("configuration error: {detail}"),
583 details: ErrorDetails::Config,
584 cause: None,
585 }
586 }
587
588 pub fn bad_request(detail: impl fmt::Display) -> Self {
589 Self {
590 code: ErrorCode::BAD_REQUEST,
591 message: format!("bad request: {detail}"),
592 details: ErrorDetails::BadRequest,
593 cause: None,
594 }
595 }
596
597 pub fn no_leader(detail: impl fmt::Display) -> Self {
600 Self {
601 code: ErrorCode::NO_LEADER,
602 message: format!("no serving leader: {detail}"),
603 details: ErrorDetails::NoLeader,
604 cause: None,
605 }
606 }
607
608 pub fn not_leader(leader_addr: impl Into<String>) -> Self {
609 let leader_addr = leader_addr.into();
610 Self {
611 code: ErrorCode::NOT_LEADER,
612 message: format!("not leader; redirect to leader at {leader_addr}"),
613 details: ErrorDetails::NotLeader { leader_addr },
614 cause: None,
615 }
616 }
617
618 pub fn migration_in_progress(detail: impl fmt::Display) -> Self {
619 Self {
620 code: ErrorCode::MIGRATION_IN_PROGRESS,
621 message: format!("migration in progress: {detail}"),
622 details: ErrorDetails::MigrationInProgress,
623 cause: None,
624 }
625 }
626
627 pub fn node_unreachable(detail: impl fmt::Display) -> Self {
628 Self {
629 code: ErrorCode::NODE_UNREACHABLE,
630 message: format!("node unreachable: {detail}"),
631 details: ErrorDetails::NodeUnreachable,
632 cause: None,
633 }
634 }
635
636 pub fn cluster(detail: impl fmt::Display) -> Self {
637 Self {
638 code: ErrorCode::CLUSTER,
639 message: format!("cluster error: {detail}"),
640 details: ErrorDetails::Cluster,
641 cause: None,
642 }
643 }
644
645 pub fn memory_exhausted(engine: impl Into<String>) -> Self {
648 let engine = engine.into();
649 Self {
650 code: ErrorCode::MEMORY_EXHAUSTED,
651 message: format!("memory budget exhausted for engine {engine}"),
652 details: ErrorDetails::MemoryExhausted { engine },
653 cause: None,
654 }
655 }
656
657 pub fn encryption(detail: impl fmt::Display) -> Self {
660 Self {
661 code: ErrorCode::ENCRYPTION,
662 message: format!("encryption error: {detail}"),
663 details: ErrorDetails::Encryption,
664 cause: None,
665 }
666 }
667
668 pub fn bridge(detail: impl fmt::Display) -> Self {
671 Self {
672 code: ErrorCode::BRIDGE,
673 message: format!("bridge error: {detail}"),
674 details: ErrorDetails::Bridge,
675 cause: None,
676 }
677 }
678
679 pub fn dispatch(detail: impl fmt::Display) -> Self {
680 Self {
681 code: ErrorCode::DISPATCH,
682 message: format!("dispatch error: {detail}"),
683 details: ErrorDetails::Dispatch,
684 cause: None,
685 }
686 }
687
688 pub fn internal(detail: impl fmt::Display) -> Self {
689 Self {
690 code: ErrorCode::INTERNAL,
691 message: format!("internal error: {detail}"),
692 details: ErrorDetails::Internal,
693 cause: None,
694 }
695 }
696}
697
698pub type NodeDbResult<T> = std::result::Result<T, NodeDbError>;
700
701impl From<std::io::Error> for NodeDbError {
702 fn from(e: std::io::Error) -> Self {
703 Self::storage(e)
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710
711 #[test]
712 fn error_code_display() {
713 assert_eq!(ErrorCode::CONSTRAINT_VIOLATION.to_string(), "NDB-1000");
714 assert_eq!(ErrorCode::INTERNAL.to_string(), "NDB-9000");
715 assert_eq!(ErrorCode::WAL.to_string(), "NDB-4100");
716 }
717
718 #[test]
719 fn error_display_includes_code() {
720 let e = NodeDbError::constraint_violation("users", "duplicate email");
721 let msg = e.to_string();
722 assert!(msg.contains("NDB-1000"));
723 assert!(msg.contains("constraint violation"));
724 assert!(msg.contains("users"));
725 }
726
727 #[test]
728 fn error_code_accessor() {
729 let e = NodeDbError::write_conflict("orders", "abc");
730 assert_eq!(e.code(), ErrorCode::WRITE_CONFLICT);
731 assert_eq!(e.code().0, 1001);
732 }
733
734 #[test]
735 fn details_matching() {
736 let e = NodeDbError::collection_not_found("users");
737 assert!(matches!(
738 e.details(),
739 ErrorDetails::CollectionNotFound { collection } if collection == "users"
740 ));
741 assert!(e.is_not_found());
742 }
743
744 #[test]
745 fn error_cause_chaining() {
746 let inner = NodeDbError::storage("disk full");
747 let outer = NodeDbError::internal("write failed").with_cause(inner);
748 assert!(outer.cause().is_some());
749 assert!(outer.cause().unwrap().is_storage());
750 assert!(outer.cause().unwrap().message().contains("disk full"));
751 }
752
753 #[test]
754 fn sync_delta_rejected() {
755 let e = NodeDbError::sync_delta_rejected(
756 "unique violation",
757 Some(
758 crate::sync::compensation::CompensationHint::UniqueViolation {
759 field: "email".into(),
760 conflicting_value: "a@b.com".into(),
761 },
762 ),
763 );
764 assert!(e.to_string().contains("NDB-3001"));
765 assert!(e.to_string().contains("sync delta rejected"));
766 }
767
768 #[test]
769 fn sql_not_enabled() {
770 let e = NodeDbError::sql_not_enabled();
771 assert!(e.to_string().contains("SQL not enabled"));
772 assert_eq!(e.code(), ErrorCode::SQL_NOT_ENABLED);
773 }
774
775 #[test]
776 fn io_error_converts() {
777 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
778 let e: NodeDbError = io_err.into();
779 assert!(e.is_storage());
780 assert_eq!(e.code(), ErrorCode::STORAGE);
781 }
782
783 #[test]
784 fn retriable_errors() {
785 assert!(NodeDbError::write_conflict("x", "y").is_retriable());
786 assert!(NodeDbError::deadline_exceeded().is_retriable());
787 assert!(!NodeDbError::bad_request("bad").is_retriable());
788 }
789
790 #[test]
791 fn client_errors() {
792 assert!(NodeDbError::bad_request("bad").is_client_error());
793 assert!(!NodeDbError::internal("oops").is_client_error());
794 }
795
796 #[test]
797 fn json_serialization() {
798 let e = NodeDbError::collection_not_found("users");
799 let json = serde_json::to_value(&e).unwrap();
800 assert_eq!(json["code"], 1100);
801 assert!(json["message"].as_str().unwrap().contains("users"));
802 assert_eq!(json["details"]["kind"], "collection_not_found");
803 assert_eq!(json["details"]["collection"], "users");
804 }
805}