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 pub const APPEND_ONLY_VIOLATION: Self = Self(1010);
57 pub const BALANCE_VIOLATION: Self = Self(1011);
58 pub const PERIOD_LOCKED: Self = Self(1012);
59 pub const STATE_TRANSITION_VIOLATION: Self = Self(1013);
60 pub const TRANSITION_CHECK_VIOLATION: Self = Self(1014);
61 pub const RETENTION_VIOLATION: Self = Self(1015);
62 pub const LEGAL_HOLD_ACTIVE: Self = Self(1016);
63 pub const TYPE_MISMATCH: Self = Self(1020);
64 pub const OVERFLOW: Self = Self(1021);
65 pub const INSUFFICIENT_BALANCE: Self = Self(1022);
66 pub const RATE_EXCEEDED: Self = Self(1023);
67 pub const TYPE_GUARD_VIOLATION: Self = Self(1024);
68
69 pub const COLLECTION_NOT_FOUND: Self = Self(1100);
71 pub const DOCUMENT_NOT_FOUND: Self = Self(1101);
72
73 pub const PLAN_ERROR: Self = Self(1200);
75 pub const FAN_OUT_EXCEEDED: Self = Self(1201);
76 pub const SQL_NOT_ENABLED: Self = Self(1202);
77
78 pub const AUTHORIZATION_DENIED: Self = Self(2000);
80 pub const AUTH_EXPIRED: Self = Self(2001);
81
82 pub const SYNC_CONNECTION_FAILED: Self = Self(3000);
84 pub const SYNC_DELTA_REJECTED: Self = Self(3001);
85 pub const SHAPE_SUBSCRIPTION_FAILED: Self = Self(3002);
86
87 pub const STORAGE: Self = Self(4000);
89 pub const SEGMENT_CORRUPTED: Self = Self(4001);
90 pub const COLD_STORAGE: Self = Self(4002);
91
92 pub const WAL: Self = Self(4100);
94
95 pub const SERIALIZATION: Self = Self(4200);
97 pub const CODEC: Self = Self(4201);
98
99 pub const CONFIG: Self = Self(5000);
101 pub const BAD_REQUEST: Self = Self(5001);
102
103 pub const NO_LEADER: Self = Self(6000);
105 pub const NOT_LEADER: Self = Self(6001);
106 pub const MIGRATION_IN_PROGRESS: Self = Self(6002);
107 pub const NODE_UNREACHABLE: Self = Self(6003);
108 pub const CLUSTER: Self = Self(6010);
109
110 pub const MEMORY_EXHAUSTED: Self = Self(7000);
112
113 pub const ENCRYPTION: Self = Self(8000);
115
116 pub const INTERNAL: Self = Self(9000);
118 pub const BRIDGE: Self = Self(9001);
119 pub const DISPATCH: Self = Self(9002);
120}
121
122impl fmt::Display for ErrorCode {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 write!(f, "NDB-{:04}", self.0)
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(tag = "kind", rename_all = "snake_case")]
139pub enum ErrorDetails {
140 ConstraintViolation {
142 collection: String,
143 },
144 WriteConflict {
145 collection: String,
146 document_id: String,
147 },
148 DeadlineExceeded,
149 PrevalidationRejected {
150 constraint: String,
151 },
152 AppendOnlyViolation {
153 collection: String,
154 },
155 BalanceViolation {
156 collection: String,
157 },
158 PeriodLocked {
159 collection: String,
160 },
161 StateTransitionViolation {
162 collection: String,
163 },
164 TransitionCheckViolation {
165 collection: String,
166 },
167 TypeGuardViolation {
168 collection: String,
169 },
170 RetentionViolation {
171 collection: String,
172 },
173 LegalHoldActive {
174 collection: String,
175 },
176 TypeMismatch {
177 collection: String,
178 },
179 Overflow {
180 collection: String,
181 },
182 InsufficientBalance {
183 collection: String,
184 },
185 RateExceeded {
186 gate: String,
187 },
188
189 CollectionNotFound {
191 collection: String,
192 },
193 DocumentNotFound {
194 collection: String,
195 document_id: String,
196 },
197
198 PlanError,
200 FanOutExceeded {
201 shards_touched: u16,
202 limit: u16,
203 },
204 SqlNotEnabled,
205
206 AuthorizationDenied {
208 resource: String,
209 },
210 AuthExpired,
211
212 SyncConnectionFailed,
214 SyncDeltaRejected {
215 compensation: Option<crate::sync::compensation::CompensationHint>,
216 },
217 ShapeSubscriptionFailed {
218 shape_id: String,
219 },
220
221 Storage,
223 SegmentCorrupted,
224 ColdStorage,
225 Wal,
226
227 Serialization {
229 format: String,
230 },
231 Codec,
232
233 Config,
235 BadRequest,
236
237 NoLeader,
239 NotLeader {
240 leader_addr: String,
241 },
242 MigrationInProgress,
243 NodeUnreachable,
244 Cluster,
245
246 MemoryExhausted {
248 engine: String,
249 },
250
251 Encryption,
253
254 Bridge,
256 Dispatch,
257 Internal,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct NodeDbError {
270 code: ErrorCode,
271 message: String,
272 details: ErrorDetails,
273 #[serde(skip_serializing_if = "Option::is_none")]
274 cause: Option<Box<NodeDbError>>,
275}
276
277impl fmt::Display for NodeDbError {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 write!(f, "[{}] {}", self.code, self.message)
280 }
281}
282
283impl std::error::Error for NodeDbError {
284 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
285 self.cause.as_deref().map(|e| e as &dyn std::error::Error)
286 }
287}
288
289impl NodeDbError {
294 pub fn code(&self) -> ErrorCode {
296 self.code
297 }
298
299 pub fn message(&self) -> &str {
301 &self.message
302 }
303
304 pub fn details(&self) -> &ErrorDetails {
306 &self.details
307 }
308
309 pub fn cause(&self) -> Option<&NodeDbError> {
311 self.cause.as_deref()
312 }
313
314 pub fn with_cause(mut self, cause: NodeDbError) -> Self {
316 self.cause = Some(Box::new(cause));
317 self
318 }
319
320 pub fn is_retriable(&self) -> bool {
322 matches!(
323 self.details,
324 ErrorDetails::WriteConflict { .. }
325 | ErrorDetails::DeadlineExceeded
326 | ErrorDetails::NoLeader
327 | ErrorDetails::NotLeader { .. }
328 | ErrorDetails::MigrationInProgress
329 | ErrorDetails::NodeUnreachable
330 | ErrorDetails::SyncConnectionFailed
331 | ErrorDetails::Bridge
332 | ErrorDetails::MemoryExhausted { .. }
333 )
334 }
335
336 pub fn is_client_error(&self) -> bool {
338 matches!(
339 self.details,
340 ErrorDetails::BadRequest
341 | ErrorDetails::ConstraintViolation { .. }
342 | ErrorDetails::AppendOnlyViolation { .. }
343 | ErrorDetails::BalanceViolation { .. }
344 | ErrorDetails::PeriodLocked { .. }
345 | ErrorDetails::StateTransitionViolation { .. }
346 | ErrorDetails::TransitionCheckViolation { .. }
347 | ErrorDetails::RetentionViolation { .. }
348 | ErrorDetails::LegalHoldActive { .. }
349 | ErrorDetails::CollectionNotFound { .. }
350 | ErrorDetails::DocumentNotFound { .. }
351 | ErrorDetails::AuthorizationDenied { .. }
352 | ErrorDetails::AuthExpired
353 | ErrorDetails::Config
354 | ErrorDetails::SqlNotEnabled
355 )
356 }
357}
358
359impl NodeDbError {
364 pub fn is_constraint_violation(&self) -> bool {
365 matches!(self.details, ErrorDetails::ConstraintViolation { .. })
366 }
367 pub fn is_not_found(&self) -> bool {
368 matches!(
369 self.details,
370 ErrorDetails::CollectionNotFound { .. } | ErrorDetails::DocumentNotFound { .. }
371 )
372 }
373 pub fn is_auth_denied(&self) -> bool {
374 matches!(self.details, ErrorDetails::AuthorizationDenied { .. })
375 }
376 pub fn is_storage(&self) -> bool {
377 matches!(
378 self.details,
379 ErrorDetails::Storage
380 | ErrorDetails::SegmentCorrupted
381 | ErrorDetails::ColdStorage
382 | ErrorDetails::Wal
383 )
384 }
385 pub fn is_internal(&self) -> bool {
386 matches!(self.details, ErrorDetails::Internal)
387 }
388 pub fn is_type_mismatch(&self) -> bool {
389 matches!(self.details, ErrorDetails::TypeMismatch { .. })
390 }
391 pub fn is_type_guard_violation(&self) -> bool {
392 matches!(self.details, ErrorDetails::TypeGuardViolation { .. })
393 }
394 pub fn is_overflow(&self) -> bool {
395 matches!(self.details, ErrorDetails::Overflow { .. })
396 }
397 pub fn is_insufficient_balance(&self) -> bool {
398 matches!(self.details, ErrorDetails::InsufficientBalance { .. })
399 }
400 pub fn is_rate_exceeded(&self) -> bool {
401 matches!(self.details, ErrorDetails::RateExceeded { .. })
402 }
403 pub fn is_cluster(&self) -> bool {
404 matches!(
405 self.details,
406 ErrorDetails::NoLeader
407 | ErrorDetails::NotLeader { .. }
408 | ErrorDetails::MigrationInProgress
409 | ErrorDetails::NodeUnreachable
410 | ErrorDetails::Cluster
411 )
412 }
413}
414
415impl NodeDbError {
420 pub fn constraint_violation(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
423 let collection = collection.into();
424 Self {
425 code: ErrorCode::CONSTRAINT_VIOLATION,
426 message: format!("constraint violation on {collection}: {detail}"),
427 details: ErrorDetails::ConstraintViolation { collection },
428 cause: None,
429 }
430 }
431
432 pub fn write_conflict(collection: impl Into<String>, document_id: impl Into<String>) -> Self {
433 let collection = collection.into();
434 let document_id = document_id.into();
435 Self {
436 code: ErrorCode::WRITE_CONFLICT,
437 message: format!(
438 "write conflict on {collection}/{document_id}, retry with idempotency key"
439 ),
440 details: ErrorDetails::WriteConflict {
441 collection,
442 document_id,
443 },
444 cause: None,
445 }
446 }
447
448 pub fn deadline_exceeded() -> Self {
449 Self {
450 code: ErrorCode::DEADLINE_EXCEEDED,
451 message: "request exceeded deadline".into(),
452 details: ErrorDetails::DeadlineExceeded,
453 cause: None,
454 }
455 }
456
457 pub fn prevalidation_rejected(
458 constraint: impl Into<String>,
459 reason: impl fmt::Display,
460 ) -> Self {
461 let constraint = constraint.into();
462 Self {
463 code: ErrorCode::PREVALIDATION_REJECTED,
464 message: format!("pre-validation rejected: {constraint} — {reason}"),
465 details: ErrorDetails::PrevalidationRejected { constraint },
466 cause: None,
467 }
468 }
469
470 pub fn append_only_violation(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
473 let collection = collection.into();
474 Self {
475 code: ErrorCode::APPEND_ONLY_VIOLATION,
476 message: format!("append-only violation on {collection}: {detail}"),
477 details: ErrorDetails::AppendOnlyViolation { collection },
478 cause: None,
479 }
480 }
481
482 pub fn balance_violation(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
483 let collection = collection.into();
484 Self {
485 code: ErrorCode::BALANCE_VIOLATION,
486 message: format!("balance violation on {collection}: {detail}"),
487 details: ErrorDetails::BalanceViolation { collection },
488 cause: None,
489 }
490 }
491
492 pub fn period_locked(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
493 let collection = collection.into();
494 Self {
495 code: ErrorCode::PERIOD_LOCKED,
496 message: format!("period locked on {collection}: {detail}"),
497 details: ErrorDetails::PeriodLocked { collection },
498 cause: None,
499 }
500 }
501
502 pub fn state_transition_violation(
503 collection: impl Into<String>,
504 detail: impl fmt::Display,
505 ) -> Self {
506 let collection = collection.into();
507 Self {
508 code: ErrorCode::STATE_TRANSITION_VIOLATION,
509 message: format!("state transition violation on {collection}: {detail}"),
510 details: ErrorDetails::StateTransitionViolation { collection },
511 cause: None,
512 }
513 }
514
515 pub fn transition_check_violation(
516 collection: impl Into<String>,
517 detail: impl fmt::Display,
518 ) -> Self {
519 let collection = collection.into();
520 Self {
521 code: ErrorCode::TRANSITION_CHECK_VIOLATION,
522 message: format!("transition check violation on {collection}: {detail}"),
523 details: ErrorDetails::TransitionCheckViolation { collection },
524 cause: None,
525 }
526 }
527
528 pub fn type_guard_violation(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
529 let collection = collection.into();
530 Self {
531 code: ErrorCode::TYPE_GUARD_VIOLATION,
532 message: format!("type guard violation on {collection}: {detail}"),
533 details: ErrorDetails::TypeGuardViolation { collection },
534 cause: None,
535 }
536 }
537
538 pub fn retention_violation(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
539 let collection = collection.into();
540 Self {
541 code: ErrorCode::RETENTION_VIOLATION,
542 message: format!("retention violation on {collection}: {detail}"),
543 details: ErrorDetails::RetentionViolation { collection },
544 cause: None,
545 }
546 }
547
548 pub fn legal_hold_active(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
549 let collection = collection.into();
550 Self {
551 code: ErrorCode::LEGAL_HOLD_ACTIVE,
552 message: format!("legal hold active on {collection}: {detail}"),
553 details: ErrorDetails::LegalHoldActive { collection },
554 cause: None,
555 }
556 }
557
558 pub fn type_mismatch(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
559 let collection = collection.into();
560 Self {
561 code: ErrorCode::TYPE_MISMATCH,
562 message: format!("type mismatch on {collection}: {detail}"),
563 details: ErrorDetails::TypeMismatch { collection },
564 cause: None,
565 }
566 }
567
568 pub fn overflow(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
569 let collection = collection.into();
570 Self {
571 code: ErrorCode::OVERFLOW,
572 message: format!("arithmetic overflow on {collection}: {detail}"),
573 details: ErrorDetails::Overflow { collection },
574 cause: None,
575 }
576 }
577
578 pub fn insufficient_balance(collection: impl Into<String>, detail: impl fmt::Display) -> Self {
579 let collection = collection.into();
580 Self {
581 code: ErrorCode::INSUFFICIENT_BALANCE,
582 message: format!("insufficient balance on {collection}: {detail}"),
583 details: ErrorDetails::InsufficientBalance { collection },
584 cause: None,
585 }
586 }
587
588 pub fn rate_exceeded(gate: impl Into<String>, detail: impl fmt::Display) -> Self {
589 let gate = gate.into();
590 Self {
591 code: ErrorCode::RATE_EXCEEDED,
592 message: format!("rate limit exceeded for {gate}: {detail}"),
593 details: ErrorDetails::RateExceeded { gate },
594 cause: None,
595 }
596 }
597
598 pub fn collection_not_found(collection: impl Into<String>) -> Self {
601 let collection = collection.into();
602 Self {
603 code: ErrorCode::COLLECTION_NOT_FOUND,
604 message: format!("collection '{collection}' not found"),
605 details: ErrorDetails::CollectionNotFound { collection },
606 cause: None,
607 }
608 }
609
610 pub fn document_not_found(
611 collection: impl Into<String>,
612 document_id: impl Into<String>,
613 ) -> Self {
614 let collection = collection.into();
615 let document_id = document_id.into();
616 Self {
617 code: ErrorCode::DOCUMENT_NOT_FOUND,
618 message: format!("document '{document_id}' not found in '{collection}'"),
619 details: ErrorDetails::DocumentNotFound {
620 collection,
621 document_id,
622 },
623 cause: None,
624 }
625 }
626
627 pub fn plan_error(detail: impl fmt::Display) -> Self {
630 Self {
631 code: ErrorCode::PLAN_ERROR,
632 message: format!("query plan error: {detail}"),
633 details: ErrorDetails::PlanError,
634 cause: None,
635 }
636 }
637
638 pub fn fan_out_exceeded(shards_touched: u16, limit: u16) -> Self {
639 Self {
640 code: ErrorCode::FAN_OUT_EXCEEDED,
641 message: format!("query fan-out exceeded: {shards_touched} shards > limit {limit}"),
642 details: ErrorDetails::FanOutExceeded {
643 shards_touched,
644 limit,
645 },
646 cause: None,
647 }
648 }
649
650 pub fn sql_not_enabled() -> Self {
651 Self {
652 code: ErrorCode::SQL_NOT_ENABLED,
653 message: "SQL not enabled (compile with 'sql' feature)".into(),
654 details: ErrorDetails::SqlNotEnabled,
655 cause: None,
656 }
657 }
658
659 pub fn authorization_denied(resource: impl Into<String>) -> Self {
662 let resource = resource.into();
663 Self {
664 code: ErrorCode::AUTHORIZATION_DENIED,
665 message: format!("authorization denied on {resource}"),
666 details: ErrorDetails::AuthorizationDenied { resource },
667 cause: None,
668 }
669 }
670
671 pub fn auth_expired(detail: impl fmt::Display) -> Self {
672 Self {
673 code: ErrorCode::AUTH_EXPIRED,
674 message: format!("auth expired: {detail}"),
675 details: ErrorDetails::AuthExpired,
676 cause: None,
677 }
678 }
679
680 pub fn sync_connection_failed(detail: impl fmt::Display) -> Self {
683 Self {
684 code: ErrorCode::SYNC_CONNECTION_FAILED,
685 message: format!("sync connection failed: {detail}"),
686 details: ErrorDetails::SyncConnectionFailed,
687 cause: None,
688 }
689 }
690
691 pub fn sync_delta_rejected(
692 reason: impl fmt::Display,
693 compensation: Option<crate::sync::compensation::CompensationHint>,
694 ) -> Self {
695 Self {
696 code: ErrorCode::SYNC_DELTA_REJECTED,
697 message: format!("sync delta rejected: {reason}"),
698 details: ErrorDetails::SyncDeltaRejected { compensation },
699 cause: None,
700 }
701 }
702
703 pub fn shape_subscription_failed(
704 shape_id: impl Into<String>,
705 detail: impl fmt::Display,
706 ) -> Self {
707 let shape_id = shape_id.into();
708 Self {
709 code: ErrorCode::SHAPE_SUBSCRIPTION_FAILED,
710 message: format!("shape subscription failed for '{shape_id}': {detail}"),
711 details: ErrorDetails::ShapeSubscriptionFailed { shape_id },
712 cause: None,
713 }
714 }
715
716 pub fn storage(detail: impl fmt::Display) -> Self {
719 Self {
720 code: ErrorCode::STORAGE,
721 message: format!("storage error: {detail}"),
722 details: ErrorDetails::Storage,
723 cause: None,
724 }
725 }
726
727 pub fn segment_corrupted(detail: impl fmt::Display) -> Self {
728 Self {
729 code: ErrorCode::SEGMENT_CORRUPTED,
730 message: format!("segment corrupted: {detail}"),
731 details: ErrorDetails::SegmentCorrupted,
732 cause: None,
733 }
734 }
735
736 pub fn cold_storage(detail: impl fmt::Display) -> Self {
737 Self {
738 code: ErrorCode::COLD_STORAGE,
739 message: format!("cold storage error: {detail}"),
740 details: ErrorDetails::ColdStorage,
741 cause: None,
742 }
743 }
744
745 pub fn wal(detail: impl fmt::Display) -> Self {
746 Self {
747 code: ErrorCode::WAL,
748 message: format!("WAL error: {detail}"),
749 details: ErrorDetails::Wal,
750 cause: None,
751 }
752 }
753
754 pub fn serialization(format: impl Into<String>, detail: impl fmt::Display) -> Self {
757 let format = format.into();
758 Self {
759 code: ErrorCode::SERIALIZATION,
760 message: format!("serialization error ({format}): {detail}"),
761 details: ErrorDetails::Serialization { format },
762 cause: None,
763 }
764 }
765
766 pub fn codec(detail: impl fmt::Display) -> Self {
767 Self {
768 code: ErrorCode::CODEC,
769 message: format!("codec error: {detail}"),
770 details: ErrorDetails::Codec,
771 cause: None,
772 }
773 }
774
775 pub fn config(detail: impl fmt::Display) -> Self {
778 Self {
779 code: ErrorCode::CONFIG,
780 message: format!("configuration error: {detail}"),
781 details: ErrorDetails::Config,
782 cause: None,
783 }
784 }
785
786 pub fn bad_request(detail: impl fmt::Display) -> Self {
787 Self {
788 code: ErrorCode::BAD_REQUEST,
789 message: format!("bad request: {detail}"),
790 details: ErrorDetails::BadRequest,
791 cause: None,
792 }
793 }
794
795 pub fn no_leader(detail: impl fmt::Display) -> Self {
798 Self {
799 code: ErrorCode::NO_LEADER,
800 message: format!("no serving leader: {detail}"),
801 details: ErrorDetails::NoLeader,
802 cause: None,
803 }
804 }
805
806 pub fn not_leader(leader_addr: impl Into<String>) -> Self {
807 let leader_addr = leader_addr.into();
808 Self {
809 code: ErrorCode::NOT_LEADER,
810 message: format!("not leader; redirect to leader at {leader_addr}"),
811 details: ErrorDetails::NotLeader { leader_addr },
812 cause: None,
813 }
814 }
815
816 pub fn migration_in_progress(detail: impl fmt::Display) -> Self {
817 Self {
818 code: ErrorCode::MIGRATION_IN_PROGRESS,
819 message: format!("migration in progress: {detail}"),
820 details: ErrorDetails::MigrationInProgress,
821 cause: None,
822 }
823 }
824
825 pub fn node_unreachable(detail: impl fmt::Display) -> Self {
826 Self {
827 code: ErrorCode::NODE_UNREACHABLE,
828 message: format!("node unreachable: {detail}"),
829 details: ErrorDetails::NodeUnreachable,
830 cause: None,
831 }
832 }
833
834 pub fn cluster(detail: impl fmt::Display) -> Self {
835 Self {
836 code: ErrorCode::CLUSTER,
837 message: format!("cluster error: {detail}"),
838 details: ErrorDetails::Cluster,
839 cause: None,
840 }
841 }
842
843 pub fn memory_exhausted(engine: impl Into<String>) -> Self {
846 let engine = engine.into();
847 Self {
848 code: ErrorCode::MEMORY_EXHAUSTED,
849 message: format!("memory budget exhausted for engine {engine}"),
850 details: ErrorDetails::MemoryExhausted { engine },
851 cause: None,
852 }
853 }
854
855 pub fn encryption(detail: impl fmt::Display) -> Self {
858 Self {
859 code: ErrorCode::ENCRYPTION,
860 message: format!("encryption error: {detail}"),
861 details: ErrorDetails::Encryption,
862 cause: None,
863 }
864 }
865
866 pub fn bridge(detail: impl fmt::Display) -> Self {
869 Self {
870 code: ErrorCode::BRIDGE,
871 message: format!("bridge error: {detail}"),
872 details: ErrorDetails::Bridge,
873 cause: None,
874 }
875 }
876
877 pub fn dispatch(detail: impl fmt::Display) -> Self {
878 Self {
879 code: ErrorCode::DISPATCH,
880 message: format!("dispatch error: {detail}"),
881 details: ErrorDetails::Dispatch,
882 cause: None,
883 }
884 }
885
886 pub fn internal(detail: impl fmt::Display) -> Self {
887 Self {
888 code: ErrorCode::INTERNAL,
889 message: format!("internal error: {detail}"),
890 details: ErrorDetails::Internal,
891 cause: None,
892 }
893 }
894}
895
896pub type NodeDbResult<T> = std::result::Result<T, NodeDbError>;
898
899impl From<std::io::Error> for NodeDbError {
900 fn from(e: std::io::Error) -> Self {
901 Self::storage(e)
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908
909 #[test]
910 fn error_code_display() {
911 assert_eq!(ErrorCode::CONSTRAINT_VIOLATION.to_string(), "NDB-1000");
912 assert_eq!(ErrorCode::INTERNAL.to_string(), "NDB-9000");
913 assert_eq!(ErrorCode::WAL.to_string(), "NDB-4100");
914 }
915
916 #[test]
917 fn error_display_includes_code() {
918 let e = NodeDbError::constraint_violation("users", "duplicate email");
919 let msg = e.to_string();
920 assert!(msg.contains("NDB-1000"));
921 assert!(msg.contains("constraint violation"));
922 assert!(msg.contains("users"));
923 }
924
925 #[test]
926 fn error_code_accessor() {
927 let e = NodeDbError::write_conflict("orders", "abc");
928 assert_eq!(e.code(), ErrorCode::WRITE_CONFLICT);
929 assert_eq!(e.code().0, 1001);
930 }
931
932 #[test]
933 fn details_matching() {
934 let e = NodeDbError::collection_not_found("users");
935 assert!(matches!(
936 e.details(),
937 ErrorDetails::CollectionNotFound { collection } if collection == "users"
938 ));
939 assert!(e.is_not_found());
940 }
941
942 #[test]
943 fn error_cause_chaining() {
944 let inner = NodeDbError::storage("disk full");
945 let outer = NodeDbError::internal("write failed").with_cause(inner);
946 assert!(outer.cause().is_some());
947 assert!(outer.cause().unwrap().is_storage());
948 assert!(outer.cause().unwrap().message().contains("disk full"));
949 }
950
951 #[test]
952 fn sync_delta_rejected() {
953 let e = NodeDbError::sync_delta_rejected(
954 "unique violation",
955 Some(
956 crate::sync::compensation::CompensationHint::UniqueViolation {
957 field: "email".into(),
958 conflicting_value: "a@b.com".into(),
959 },
960 ),
961 );
962 assert!(e.to_string().contains("NDB-3001"));
963 assert!(e.to_string().contains("sync delta rejected"));
964 }
965
966 #[test]
967 fn sql_not_enabled() {
968 let e = NodeDbError::sql_not_enabled();
969 assert!(e.to_string().contains("SQL not enabled"));
970 assert_eq!(e.code(), ErrorCode::SQL_NOT_ENABLED);
971 }
972
973 #[test]
974 fn io_error_converts() {
975 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
976 let e: NodeDbError = io_err.into();
977 assert!(e.is_storage());
978 assert_eq!(e.code(), ErrorCode::STORAGE);
979 }
980
981 #[test]
982 fn retriable_errors() {
983 assert!(NodeDbError::write_conflict("x", "y").is_retriable());
984 assert!(NodeDbError::deadline_exceeded().is_retriable());
985 assert!(!NodeDbError::bad_request("bad").is_retriable());
986 }
987
988 #[test]
989 fn client_errors() {
990 assert!(NodeDbError::bad_request("bad").is_client_error());
991 assert!(!NodeDbError::internal("oops").is_client_error());
992 }
993
994 #[test]
995 fn json_serialization() {
996 let e = NodeDbError::collection_not_found("users");
997 let json = serde_json::to_value(&e).unwrap();
998 assert_eq!(json["code"], 1100);
999 assert!(json["message"].as_str().unwrap().contains("users"));
1000 assert_eq!(json["details"]["kind"], "collection_not_found");
1001 assert_eq!(json["details"]["collection"], "users");
1002 }
1003}