1use parking_lot::RwLock;
6use std::error::Error;
7use std::fmt;
8use std::io;
9use std::sync::{Arc, OnceLock};
10
11type ErrorHook = Arc<dyn Fn(&DbError) + Send + Sync>;
13
14static GLOBAL_ERROR_HOOK: OnceLock<RwLock<Option<ErrorHook>>> = OnceLock::new();
19
20fn error_hook_storage() -> &'static RwLock<Option<ErrorHook>> {
22 GLOBAL_ERROR_HOOK.get_or_init(|| RwLock::new(None))
23}
24
25pub fn set_error_hook(hook: ErrorHook) {
29 *error_hook_storage().write() = Some(hook);
30}
31
32pub fn trigger_error_hook(err: &DbError) {
36 let storage = error_hook_storage().read();
37 if let Some(ref hook) = *storage {
38 hook(err);
39 }
40}
41
42#[derive(Debug)]
44pub enum DbError {
45 QueryError(String),
47
48 ConnectionError(String),
50
51 ConnectionRefused(String),
53
54 ConnectionTimeout(String),
56
57 PoolError(PoolError),
59
60 CacheError(CacheError),
62
63 TxError(TxError),
65
66 MigrationError(String),
68
69 Unsupported(String),
71
72 ConfigError(String),
74
75 SerdeError(String),
77
78 NotFound(String),
80
81 AlreadyExists(String),
83
84 ConstraintViolation(String),
86
87 UniqueViolation(String),
89
90 ForeignKeyViolation(String),
92
93 NullValue(String),
95
96 InvalidInput(String),
98
99 Internal(String),
101
102 IoError(String),
104
105 Hook(String),
107
108 TenantError(String),
110
111 Validation(String),
113
114 Contextual {
119 source: Box<DbError>,
121 context: ErrorContext,
123 },
124}
125
126#[derive(Debug, Clone)]
131pub struct ErrorContext {
132 pub context: String,
134 pub span: Option<String>,
136 pub previous: Option<Box<ErrorContext>>,
138}
139
140impl ErrorContext {
141 pub fn new(context: impl Into<String>) -> Self {
143 Self {
144 context: context.into(),
145 span: None,
146 previous: None,
147 }
148 }
149
150 pub fn with_previous(mut self, prev: ErrorContext) -> Self {
152 self.previous = Some(Box::new(prev));
153 self
154 }
155
156 pub fn with_span(mut self, span: impl Into<String>) -> Self {
158 self.span = Some(span.into());
159 self
160 }
161
162 pub fn iter(&self) -> impl Iterator<Item = &ErrorContext> {
164 let mut current = Some(self);
165 std::iter::from_fn(move || {
166 let node = current?;
167 let result = node;
168 current = node.previous.as_deref();
169 Some(result)
170 })
171 }
172
173 pub fn format_chain(&self) -> String {
175 self.iter()
176 .enumerate()
177 .map(|(i, ctx)| {
178 if let Some(ref span) = ctx.span {
179 format!(" [{}] {} (span: {})", i, ctx.context, span)
180 } else {
181 format!(" [{}] {}", i, ctx.context)
182 }
183 })
184 .collect::<Vec<_>>()
185 .join("\n")
186 }
187}
188
189impl DbError {
190 pub fn query(s: impl Into<String>) -> Self {
192 DbError::QueryError(s.into())
193 }
194
195 pub fn connection(s: impl Into<String>) -> Self {
197 DbError::ConnectionError(s.into())
198 }
199
200 pub fn not_found(s: impl Into<String>) -> Self {
202 DbError::NotFound(s.into())
203 }
204
205 pub fn with_context(self, context: impl Into<String>) -> Self {
221 let new_ctx = ErrorContext::new(context);
222 match self {
224 DbError::Contextual {
225 source,
226 context: existing_ctx,
227 } => {
228 let new_ctx = new_ctx.with_previous(existing_ctx);
229 DbError::Contextual {
230 source,
231 context: new_ctx,
232 }
233 }
234 other => DbError::Contextual {
235 source: Box::new(other),
236 context: new_ctx,
237 },
238 }
239 }
240
241 pub fn with_context_in_span(self, context: impl Into<String>, span: impl Into<String>) -> Self {
243 let new_ctx = ErrorContext::new(context).with_span(span);
244 match self {
245 DbError::Contextual {
246 source,
247 context: existing_ctx,
248 } => {
249 let new_ctx = new_ctx.with_previous(existing_ctx);
250 DbError::Contextual {
251 source,
252 context: new_ctx,
253 }
254 }
255 other => DbError::Contextual {
256 source: Box::new(other),
257 context: new_ctx,
258 },
259 }
260 }
261
262 pub fn context(&self) -> Option<&ErrorContext> {
264 match self {
265 DbError::Contextual { context, .. } => Some(context),
266 _ => None,
267 }
268 }
269
270 pub fn format_context_chain(&self) -> String {
272 match self {
273 DbError::Contextual { context, .. } => context.format_chain(),
274 _ => String::new(),
275 }
276 }
277
278 pub fn root_cause(&self) -> &DbError {
280 match self {
281 DbError::Contextual { source, .. } => source.root_cause(),
282 other => other,
283 }
284 }
285
286 pub fn is_retryable(&self) -> bool {
288 self.root_cause_is_retryable()
289 }
290
291 fn root_cause_is_retryable(&self) -> bool {
293 match self {
294 DbError::Contextual { source, .. } => source.root_cause_is_retryable(),
295 DbError::ConnectionError(_)
296 | DbError::ConnectionTimeout(_)
297 | DbError::PoolError(PoolError::Timeout) => true,
298 _ => false,
299 }
300 }
301
302 pub fn error_code(&self) -> &'static str {
304 match self {
305 DbError::Contextual { source, .. } => source.error_code(),
306 DbError::QueryError(_) => "DB001",
307 DbError::ConnectionError(_) => "DB002",
308 DbError::ConnectionRefused(_) => "DB003",
309 DbError::ConnectionTimeout(_) => "DB004",
310 DbError::PoolError(e) => e.error_code(),
311 DbError::CacheError(e) => e.error_code(),
312 DbError::TxError(_) => "DB007",
313 DbError::MigrationError(_) => "DB008",
314 DbError::Unsupported(_) => "DB009",
315 DbError::ConfigError(_) => "DB010",
316 DbError::SerdeError(_) => "DB011",
317 DbError::NotFound(_) => "DB012",
318 DbError::AlreadyExists(_) => "DB013",
319 DbError::ConstraintViolation(_) => "DB014",
320 DbError::UniqueViolation(_) => "DB022",
321 DbError::ForeignKeyViolation(_) => "DB023",
322 DbError::NullValue(_) => "DB015",
323 DbError::InvalidInput(_) => "DB016",
324 DbError::Internal(_) => "DB017",
325 DbError::IoError(_) => "DB018",
326 DbError::Hook(_) => "DB019",
327 DbError::TenantError(_) => "DB020",
328 DbError::Validation(_) => "DB021",
329 }
330 }
331
332 pub fn http_status(&self) -> u16 {
345 match self {
346 DbError::Contextual { source, .. } => source.http_status(),
347 DbError::InvalidInput(_) | DbError::Validation(_) | DbError::ConfigError(_) => 400,
348 DbError::NotFound(_) => 404,
349 DbError::AlreadyExists(_)
350 | DbError::ConstraintViolation(_)
351 | DbError::UniqueViolation(_)
352 | DbError::ForeignKeyViolation(_)
353 | DbError::NullValue(_) => 409,
354 DbError::SerdeError(_) => 422,
355 DbError::Unsupported(_) => 501,
356 DbError::ConnectionError(_) | DbError::ConnectionRefused(_) => 502,
357 DbError::ConnectionTimeout(_) => 504,
358 DbError::PoolError(e) => match e {
359 PoolError::Timeout => 504,
360 PoolError::Exhausted | PoolError::Closed | PoolError::ConnectionFailed(_) => 503,
361 _ => 500,
362 },
363 DbError::CacheError(_) => 503,
364 _ => 500,
366 }
367 }
368
369 pub fn grpc_status_code(&self) -> u32 {
385 match self {
386 DbError::Contextual { source, .. } => source.grpc_status_code(),
387 DbError::InvalidInput(_) | DbError::Validation(_) | DbError::ConfigError(_) => 3,
388 DbError::ConnectionTimeout(_) => 4,
389 DbError::PoolError(PoolError::Timeout) => 4,
390 DbError::NotFound(_) => 5,
391 DbError::AlreadyExists(_) | DbError::UniqueViolation(_) => 6,
392 DbError::TenantError(_) => 7,
393 DbError::PoolError(PoolError::Exhausted) | DbError::PoolError(PoolError::Closed) => 8,
394 DbError::CacheError(_) => 8,
395 DbError::ConstraintViolation(_)
396 | DbError::ForeignKeyViolation(_)
397 | DbError::NullValue(_)
398 | DbError::TxError(_) => 9,
399 DbError::Unsupported(_) => 12,
400 DbError::SerdeError(_) => 13,
401 DbError::ConnectionError(_)
402 | DbError::ConnectionRefused(_)
403 | DbError::PoolError(PoolError::ConnectionFailed(_)) => 14,
404 _ => 2,
406 }
407 }
408}
409
410impl fmt::Display for DbError {
411 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412 match self {
413 DbError::QueryError(s) => write!(f, "Query error: {}", s),
414 DbError::ConnectionError(s) => write!(f, "Connection error: {}", s),
415 DbError::ConnectionRefused(s) => write!(f, "Connection refused: {}", s),
416 DbError::ConnectionTimeout(s) => write!(f, "Connection timeout: {}", s),
417 DbError::PoolError(e) => write!(f, "Pool error: {}", e),
418 DbError::CacheError(e) => write!(f, "Cache error: {}", e),
419 DbError::TxError(e) => write!(f, "Transaction error: {}", e),
420 DbError::MigrationError(s) => write!(f, "Migration error: {}", s),
421 DbError::Unsupported(s) => write!(f, "Unsupported: {}", s),
422 DbError::ConfigError(s) => write!(f, "Configuration error: {}", s),
423 DbError::SerdeError(s) => write!(f, "Serialization error: {}", s),
424 DbError::NotFound(s) => write!(f, "Not found: {}", s),
425 DbError::AlreadyExists(s) => write!(f, "Already exists: {}", s),
426 DbError::ConstraintViolation(s) => write!(f, "Constraint violation: {}", s),
427 DbError::UniqueViolation(s) => write!(f, "Unique constraint violation: {}", s),
428 DbError::ForeignKeyViolation(s) => write!(f, "Foreign key constraint violation: {}", s),
429 DbError::NullValue(s) => write!(f, "Null value: {}", s),
430 DbError::InvalidInput(s) => write!(f, "Invalid input: {}", s),
431 DbError::Internal(s) => write!(f, "Internal error: {}", s),
432 DbError::IoError(s) => write!(f, "IO error: {}", s),
433 DbError::Hook(s) => write!(f, "Hook error: {}", s),
434 DbError::TenantError(s) => write!(f, "Tenant error: {}", s),
435 DbError::Validation(s) => write!(f, "Validation error: {}", s),
436 DbError::Contextual {
437 context, source, ..
438 } => write!(f, "{}: {}", context.context, source),
439 }
440 }
441}
442
443impl Error for DbError {
444 fn source(&self) -> Option<&(dyn Error + 'static)> {
445 match self {
446 DbError::PoolError(e) => Some(e),
447 DbError::CacheError(e) => Some(e),
448 DbError::TxError(e) => Some(e),
449 DbError::Contextual { source, .. } => Some(source.as_ref()),
452 _ => None,
453 }
454 }
455}
456
457impl From<io::Error> for DbError {
458 fn from(err: io::Error) -> Self {
459 DbError::IoError(err.to_string())
460 }
461}
462
463impl From<serde_json::Error> for DbError {
464 fn from(err: serde_json::Error) -> Self {
465 DbError::SerdeError(err.to_string())
466 }
467}
468
469impl From<std::num::TryFromIntError> for DbError {
470 fn from(err: std::num::TryFromIntError) -> Self {
471 DbError::Internal(err.to_string())
472 }
473}
474
475impl From<std::string::FromUtf8Error> for DbError {
476 fn from(err: std::string::FromUtf8Error) -> Self {
477 DbError::Internal(err.to_string())
478 }
479}
480
481impl<T> From<std::sync::PoisonError<T>> for DbError {
482 fn from(err: std::sync::PoisonError<T>) -> Self {
483 DbError::Internal(format!("RwLock/Mutex poisoned: {}", err))
484 }
485}
486
487#[derive(Debug)]
489pub enum PoolError {
490 Exhausted,
492
493 Timeout,
495
496 AlreadyAcquired,
498
499 NotAcquired,
501
502 InvalidConfig(String),
504
505 Internal(String),
507
508 Closed,
510
511 ConnectionFailed(String),
513
514 CircuitOpen,
520
521 RateLimited {
526 remaining: u64,
528 reset_at: i64,
530 },
531}
532
533impl PoolError {
534 pub fn error_code(&self) -> &'static str {
536 match self {
537 PoolError::Exhausted => "PL001",
538 PoolError::Timeout => "PL002",
539 PoolError::AlreadyAcquired => "PL003",
540 PoolError::NotAcquired => "PL004",
541 PoolError::InvalidConfig(_) => "PL005",
542 PoolError::Internal(_) => "PL006",
543 PoolError::Closed => "PL007",
544 PoolError::ConnectionFailed(_) => "PL008",
545 PoolError::CircuitOpen => "PL009",
546 PoolError::RateLimited { .. } => "PL010",
547 }
548 }
549}
550
551impl fmt::Display for PoolError {
552 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553 match self {
554 PoolError::Exhausted => write!(f, "Connection pool exhausted"),
555 PoolError::Timeout => write!(f, "Connection acquire timeout"),
556 PoolError::AlreadyAcquired => write!(f, "Connection already acquired"),
557 PoolError::NotAcquired => write!(f, "Connection not acquired"),
558 PoolError::InvalidConfig(s) => write!(f, "Invalid pool config: {}", s),
559 PoolError::Internal(s) => write!(f, "Internal pool error: {}", s),
560 PoolError::Closed => write!(f, "Connection pool closed"),
561 PoolError::ConnectionFailed(s) => write!(f, "Connection failed: {}", s),
562 PoolError::CircuitOpen => write!(f, "Circuit breaker open"),
563 PoolError::RateLimited {
564 remaining,
565 reset_at,
566 } => write!(
567 f,
568 "Rate limited (remaining: {}, reset_at: {})",
569 remaining, reset_at
570 ),
571 }
572 }
573}
574
575impl Error for PoolError {}
576
577#[derive(Debug, Clone)]
579pub enum CacheError {
580 NotFound(String),
582
583 SerializationError(String),
585
586 DeserializationError(String),
588
589 ConnectionError(String),
591
592 Timeout(String),
594
595 Internal(String),
597}
598
599impl CacheError {
600 pub fn error_code(&self) -> &'static str {
602 match self {
603 CacheError::NotFound(_) => "CH001",
604 CacheError::SerializationError(_) => "CH002",
605 CacheError::DeserializationError(_) => "CH003",
606 CacheError::ConnectionError(_) => "CH004",
607 CacheError::Timeout(_) => "CH005",
608 CacheError::Internal(_) => "CH006",
609 }
610 }
611}
612
613impl fmt::Display for CacheError {
614 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
615 match self {
616 CacheError::NotFound(s) => write!(f, "Cache key not found: {}", s),
617 CacheError::SerializationError(s) => write!(f, "Cache serialization error: {}", s),
618 CacheError::DeserializationError(s) => write!(f, "Cache deserialization error: {}", s),
619 CacheError::ConnectionError(s) => write!(f, "Cache connection error: {}", s),
620 CacheError::Timeout(s) => write!(f, "Cache timeout: {}", s),
621 CacheError::Internal(s) => write!(f, "Cache internal error: {}", s),
622 }
623 }
624}
625
626impl Error for CacheError {}
627
628impl<T> From<std::sync::PoisonError<T>> for CacheError {
629 fn from(err: std::sync::PoisonError<T>) -> Self {
630 CacheError::Internal(format!("RwLock poisoned: {}", err))
631 }
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
639pub enum TransactionState {
640 #[default]
642 Active,
643 Committed,
645 RolledBack,
647}
648
649impl fmt::Display for TransactionState {
650 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
651 match self {
652 TransactionState::Active => write!(f, "Active"),
653 TransactionState::Committed => write!(f, "Committed"),
654 TransactionState::RolledBack => write!(f, "RolledBack"),
655 }
656 }
657}
658
659#[derive(Debug)]
661pub enum TxError {
662 NotStarted,
664
665 AlreadyStarted,
667
668 CommitFailed(String),
670
671 RollbackFailed(String),
673
674 SavepointError(String),
676
677 NestedNotSupported,
679
680 NotActive(TransactionState),
682
683 InvalidSavepointName(String),
685
686 ConnectionTaken,
688
689 MaxNestingDepthExceeded {
693 current_depth: u32,
695 max_depth: u32,
697 },
698
699 DeadlockDetected {
704 attempt: u32,
706 max_attempts: u32,
708 },
709}
710
711impl fmt::Display for TxError {
712 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
713 match self {
714 TxError::NotStarted => write!(f, "Transaction not started"),
715 TxError::AlreadyStarted => write!(f, "Transaction already started"),
716 TxError::CommitFailed(s) => write!(f, "Transaction commit failed: {}", s),
717 TxError::RollbackFailed(s) => write!(f, "Transaction rollback failed: {}", s),
718 TxError::SavepointError(s) => write!(f, "Savepoint error: {}", s),
719 TxError::NestedNotSupported => write!(f, "Nested transactions not supported"),
720 TxError::NotActive(state) => {
721 write!(f, "Transaction not active (current state: {})", state)
722 }
723 TxError::InvalidSavepointName(name) => {
724 write!(
725 f,
726 "Invalid savepoint name '{}': must be non-empty, start with a letter or underscore, and contain only ASCII alphanumeric or underscore",
727 name
728 )
729 }
730 TxError::ConnectionTaken => write!(f, "Transaction connection already taken"),
731 TxError::MaxNestingDepthExceeded {
732 current_depth,
733 max_depth,
734 } => write!(
735 f,
736 "Transaction nesting depth {} exceeds maximum allowed {}",
737 current_depth, max_depth
738 ),
739 TxError::DeadlockDetected {
740 attempt,
741 max_attempts,
742 } => write!(
743 f,
744 "Deadlock detected on attempt {} of {}",
745 attempt, max_attempts
746 ),
747 }
748 }
749}
750
751impl Error for TxError {
752 fn source(&self) -> Option<&(dyn Error + 'static)> {
753 None
755 }
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761
762 #[test]
763 fn test_db_error_display() {
764 let err = DbError::query("test");
765 assert_eq!(format!("{}", err), "Query error: test");
766
767 let err = DbError::not_found("user");
768 assert_eq!(format!("{}", err), "Not found: user");
769 }
770
771 #[test]
772 fn test_db_error_code() {
773 let err = DbError::query("test");
774 assert_eq!(err.error_code(), "DB001");
775
776 let err = DbError::PoolError(PoolError::Timeout);
777 assert_eq!(err.error_code(), "PL002");
778 }
779
780 #[test]
781 fn test_db_error_source() {
782 let err = DbError::PoolError(PoolError::Timeout);
783 assert!(err.source().is_some());
784 }
785
786 #[test]
787 fn test_db_error_contextual_source_chain() {
788 let root = DbError::QueryError("table not found".to_string());
791 let wrapped = root.with_context("fetching user");
792 let outer = wrapped.with_context("user_service.fetch");
793
794 let source1 = outer.source().expect("outer should have source");
796 assert!(source1.source().is_none());
798
799 let ctx_chain = outer.context().expect("outer should have context");
801 assert_eq!(ctx_chain.context, "user_service.fetch");
802 let inner_ctx = ctx_chain
803 .previous
804 .as_ref()
805 .expect("should have previous context");
806 assert_eq!(inner_ctx.context, "fetching user");
807 assert!(inner_ctx.previous.is_none());
808
809 let root_cause = outer.root_cause();
811 assert!(matches!(root_cause, DbError::QueryError(_)));
812 }
813
814 #[test]
815 fn test_pool_error() {
816 let err = PoolError::Timeout;
817 assert_eq!(format!("{}", err), "Connection acquire timeout");
818 assert_eq!(err.error_code(), "PL002");
819 }
820
821 #[test]
822 fn test_cache_error() {
823 let err = CacheError::NotFound("key".to_string());
824 assert_eq!(format!("{}", err), "Cache key not found: key");
825 assert_eq!(err.error_code(), "CH001");
826 }
827
828 #[test]
829 fn test_error_hook_set_and_trigger() {
830 use std::sync::atomic::{AtomicU32, Ordering};
831 let counter = Arc::new(AtomicU32::new(0));
832 let c = counter.clone();
833 set_error_hook(Arc::new(move |_err: &DbError| {
834 c.fetch_add(1, Ordering::SeqCst);
835 }));
836 let err = DbError::query("hook test");
837 trigger_error_hook(&err);
838 assert_eq!(counter.load(Ordering::SeqCst), 1);
839 }
840
841 #[test]
842 fn test_error_hook_no_hook_silent() {
843 let err = DbError::query("no hook");
845 trigger_error_hook(&err);
846 }
847
848 #[test]
851 fn test_http_status_bad_request() {
852 assert_eq!(DbError::InvalidInput("bad".into()).http_status(), 400);
853 assert_eq!(DbError::Validation("fail".into()).http_status(), 400);
854 assert_eq!(DbError::ConfigError("cfg".into()).http_status(), 400);
855 }
856
857 #[test]
858 fn test_http_status_not_found() {
859 assert_eq!(DbError::NotFound("user".into()).http_status(), 404);
860 }
861
862 #[test]
863 fn test_http_status_conflict() {
864 assert_eq!(DbError::AlreadyExists("x".into()).http_status(), 409);
865 assert_eq!(DbError::ConstraintViolation("c".into()).http_status(), 409);
866 assert_eq!(DbError::UniqueViolation("u".into()).http_status(), 409);
867 assert_eq!(DbError::ForeignKeyViolation("f".into()).http_status(), 409);
868 assert_eq!(DbError::NullValue("n".into()).http_status(), 409);
869 }
870
871 #[test]
872 fn test_http_status_unprocessable() {
873 assert_eq!(DbError::SerdeError("s".into()).http_status(), 422);
874 }
875
876 #[test]
877 fn test_http_status_internal_server_error() {
878 assert_eq!(DbError::QueryError("q".into()).http_status(), 500);
879 assert_eq!(DbError::Internal("i".into()).http_status(), 500);
880 assert_eq!(DbError::Hook("h".into()).http_status(), 500);
881 assert_eq!(DbError::MigrationError("m".into()).http_status(), 500);
882 assert_eq!(DbError::IoError("io".into()).http_status(), 500);
883 assert_eq!(DbError::TxError(TxError::NotStarted).http_status(), 500);
884 assert_eq!(DbError::TenantError("t".into()).http_status(), 500);
885 }
886
887 #[test]
888 fn test_http_status_not_implemented() {
889 assert_eq!(DbError::Unsupported("feat".into()).http_status(), 501);
890 }
891
892 #[test]
893 fn test_http_status_bad_gateway() {
894 assert_eq!(DbError::ConnectionError("c".into()).http_status(), 502);
895 assert_eq!(DbError::ConnectionRefused("r".into()).http_status(), 502);
896 }
897
898 #[test]
899 fn test_http_status_service_unavailable() {
900 assert_eq!(DbError::PoolError(PoolError::Exhausted).http_status(), 503);
901 assert_eq!(DbError::PoolError(PoolError::Closed).http_status(), 503);
902 assert_eq!(
903 DbError::PoolError(PoolError::ConnectionFailed("f".into())).http_status(),
904 503
905 );
906 assert_eq!(
907 DbError::CacheError(CacheError::Internal("e".into())).http_status(),
908 503
909 );
910 }
911
912 #[test]
913 fn test_http_status_gateway_timeout() {
914 assert_eq!(DbError::ConnectionTimeout("t".into()).http_status(), 504);
915 assert_eq!(DbError::PoolError(PoolError::Timeout).http_status(), 504);
916 }
917
918 #[test]
921 fn test_grpc_status_invalid_argument() {
922 assert_eq!(DbError::InvalidInput("bad".into()).grpc_status_code(), 3);
923 assert_eq!(DbError::Validation("fail".into()).grpc_status_code(), 3);
924 assert_eq!(DbError::ConfigError("cfg".into()).grpc_status_code(), 3);
925 }
926
927 #[test]
928 fn test_grpc_status_deadline_exceeded() {
929 assert_eq!(DbError::ConnectionTimeout("t".into()).grpc_status_code(), 4);
930 assert_eq!(DbError::PoolError(PoolError::Timeout).grpc_status_code(), 4);
931 }
932
933 #[test]
934 fn test_grpc_status_not_found() {
935 assert_eq!(DbError::NotFound("user".into()).grpc_status_code(), 5);
936 }
937
938 #[test]
939 fn test_grpc_status_already_exists() {
940 assert_eq!(DbError::AlreadyExists("x".into()).grpc_status_code(), 6);
941 assert_eq!(DbError::UniqueViolation("u".into()).grpc_status_code(), 6);
942 }
943
944 #[test]
945 fn test_grpc_status_permission_denied() {
946 assert_eq!(DbError::TenantError("t".into()).grpc_status_code(), 7);
947 }
948
949 #[test]
950 fn test_grpc_status_resource_exhausted() {
951 assert_eq!(
952 DbError::PoolError(PoolError::Exhausted).grpc_status_code(),
953 8
954 );
955 assert_eq!(DbError::PoolError(PoolError::Closed).grpc_status_code(), 8);
956 assert_eq!(
957 DbError::CacheError(CacheError::Internal("e".into())).grpc_status_code(),
958 8
959 );
960 }
961
962 #[test]
963 fn test_grpc_status_failed_precondition() {
964 assert_eq!(
965 DbError::ConstraintViolation("c".into()).grpc_status_code(),
966 9
967 );
968 assert_eq!(
969 DbError::ForeignKeyViolation("f".into()).grpc_status_code(),
970 9
971 );
972 assert_eq!(DbError::NullValue("n".into()).grpc_status_code(), 9);
973 assert_eq!(DbError::TxError(TxError::NotStarted).grpc_status_code(), 9);
974 }
975
976 #[test]
977 fn test_grpc_status_unimplemented() {
978 assert_eq!(DbError::Unsupported("feat".into()).grpc_status_code(), 12);
979 }
980
981 #[test]
982 fn test_grpc_status_internal() {
983 assert_eq!(DbError::SerdeError("s".into()).grpc_status_code(), 13);
984 }
985
986 #[test]
987 fn test_grpc_status_unavailable() {
988 assert_eq!(DbError::ConnectionError("c".into()).grpc_status_code(), 14);
989 assert_eq!(
990 DbError::ConnectionRefused("r".into()).grpc_status_code(),
991 14
992 );
993 assert_eq!(
994 DbError::PoolError(PoolError::ConnectionFailed("f".into())).grpc_status_code(),
995 14
996 );
997 }
998
999 #[test]
1000 fn test_grpc_status_unknown() {
1001 assert_eq!(DbError::QueryError("q".into()).grpc_status_code(), 2);
1002 assert_eq!(DbError::Internal("i".into()).grpc_status_code(), 2);
1003 assert_eq!(DbError::Hook("h".into()).grpc_status_code(), 2);
1004 assert_eq!(DbError::MigrationError("m".into()).grpc_status_code(), 2);
1005 assert_eq!(DbError::IoError("io".into()).grpc_status_code(), 2);
1006 assert_eq!(
1008 DbError::PoolError(PoolError::AlreadyAcquired).grpc_status_code(),
1009 2
1010 );
1011 assert_eq!(
1012 DbError::PoolError(PoolError::NotAcquired).grpc_status_code(),
1013 2
1014 );
1015 assert_eq!(
1016 DbError::PoolError(PoolError::InvalidConfig("x".into())).grpc_status_code(),
1017 2
1018 );
1019 assert_eq!(
1020 DbError::PoolError(PoolError::Internal("y".into())).grpc_status_code(),
1021 2
1022 );
1023 }
1024}