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 { remaining: u64, reset_at: i64 },
526}
527
528impl PoolError {
529 pub fn error_code(&self) -> &'static str {
530 match self {
531 PoolError::Exhausted => "PL001",
532 PoolError::Timeout => "PL002",
533 PoolError::AlreadyAcquired => "PL003",
534 PoolError::NotAcquired => "PL004",
535 PoolError::InvalidConfig(_) => "PL005",
536 PoolError::Internal(_) => "PL006",
537 PoolError::Closed => "PL007",
538 PoolError::ConnectionFailed(_) => "PL008",
539 PoolError::CircuitOpen => "PL009",
540 PoolError::RateLimited { .. } => "PL010",
541 }
542 }
543}
544
545impl fmt::Display for PoolError {
546 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547 match self {
548 PoolError::Exhausted => write!(f, "Connection pool exhausted"),
549 PoolError::Timeout => write!(f, "Connection acquire timeout"),
550 PoolError::AlreadyAcquired => write!(f, "Connection already acquired"),
551 PoolError::NotAcquired => write!(f, "Connection not acquired"),
552 PoolError::InvalidConfig(s) => write!(f, "Invalid pool config: {}", s),
553 PoolError::Internal(s) => write!(f, "Internal pool error: {}", s),
554 PoolError::Closed => write!(f, "Connection pool closed"),
555 PoolError::ConnectionFailed(s) => write!(f, "Connection failed: {}", s),
556 PoolError::CircuitOpen => write!(f, "Circuit breaker open"),
557 PoolError::RateLimited {
558 remaining,
559 reset_at,
560 } => write!(
561 f,
562 "Rate limited (remaining: {}, reset_at: {})",
563 remaining, reset_at
564 ),
565 }
566 }
567}
568
569impl Error for PoolError {}
570
571#[derive(Debug, Clone)]
573pub enum CacheError {
574 NotFound(String),
576
577 SerializationError(String),
579
580 DeserializationError(String),
582
583 ConnectionError(String),
585
586 Timeout(String),
588
589 Internal(String),
591}
592
593impl CacheError {
594 pub fn error_code(&self) -> &'static str {
595 match self {
596 CacheError::NotFound(_) => "CH001",
597 CacheError::SerializationError(_) => "CH002",
598 CacheError::DeserializationError(_) => "CH003",
599 CacheError::ConnectionError(_) => "CH004",
600 CacheError::Timeout(_) => "CH005",
601 CacheError::Internal(_) => "CH006",
602 }
603 }
604}
605
606impl fmt::Display for CacheError {
607 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608 match self {
609 CacheError::NotFound(s) => write!(f, "Cache key not found: {}", s),
610 CacheError::SerializationError(s) => write!(f, "Cache serialization error: {}", s),
611 CacheError::DeserializationError(s) => write!(f, "Cache deserialization error: {}", s),
612 CacheError::ConnectionError(s) => write!(f, "Cache connection error: {}", s),
613 CacheError::Timeout(s) => write!(f, "Cache timeout: {}", s),
614 CacheError::Internal(s) => write!(f, "Cache internal error: {}", s),
615 }
616 }
617}
618
619impl Error for CacheError {}
620
621impl<T> From<std::sync::PoisonError<T>> for CacheError {
622 fn from(err: std::sync::PoisonError<T>) -> Self {
623 CacheError::Internal(format!("RwLock poisoned: {}", err))
624 }
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
632pub enum TransactionState {
633 #[default]
634 Active,
635 Committed,
636 RolledBack,
637}
638
639impl fmt::Display for TransactionState {
640 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
641 match self {
642 TransactionState::Active => write!(f, "Active"),
643 TransactionState::Committed => write!(f, "Committed"),
644 TransactionState::RolledBack => write!(f, "RolledBack"),
645 }
646 }
647}
648
649#[derive(Debug)]
651pub enum TxError {
652 NotStarted,
654
655 AlreadyStarted,
657
658 CommitFailed(String),
660
661 RollbackFailed(String),
663
664 SavepointError(String),
666
667 NestedNotSupported,
669
670 NotActive(TransactionState),
672
673 InvalidSavepointName(String),
675
676 ConnectionTaken,
678
679 MaxNestingDepthExceeded { current_depth: u32, max_depth: u32 },
683
684 DeadlockDetected { attempt: u32, max_attempts: u32 },
689}
690
691impl fmt::Display for TxError {
692 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
693 match self {
694 TxError::NotStarted => write!(f, "Transaction not started"),
695 TxError::AlreadyStarted => write!(f, "Transaction already started"),
696 TxError::CommitFailed(s) => write!(f, "Transaction commit failed: {}", s),
697 TxError::RollbackFailed(s) => write!(f, "Transaction rollback failed: {}", s),
698 TxError::SavepointError(s) => write!(f, "Savepoint error: {}", s),
699 TxError::NestedNotSupported => write!(f, "Nested transactions not supported"),
700 TxError::NotActive(state) => {
701 write!(f, "Transaction not active (current state: {})", state)
702 }
703 TxError::InvalidSavepointName(name) => {
704 write!(
705 f,
706 "Invalid savepoint name '{}': must be non-empty, start with a letter or underscore, and contain only ASCII alphanumeric or underscore",
707 name
708 )
709 }
710 TxError::ConnectionTaken => write!(f, "Transaction connection already taken"),
711 TxError::MaxNestingDepthExceeded {
712 current_depth,
713 max_depth,
714 } => write!(
715 f,
716 "Transaction nesting depth {} exceeds maximum allowed {}",
717 current_depth, max_depth
718 ),
719 TxError::DeadlockDetected {
720 attempt,
721 max_attempts,
722 } => write!(
723 f,
724 "Deadlock detected on attempt {} of {}",
725 attempt, max_attempts
726 ),
727 }
728 }
729}
730
731impl Error for TxError {
732 fn source(&self) -> Option<&(dyn Error + 'static)> {
733 None
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741
742 #[test]
743 fn test_db_error_display() {
744 let err = DbError::query("test");
745 assert_eq!(format!("{}", err), "Query error: test");
746
747 let err = DbError::not_found("user");
748 assert_eq!(format!("{}", err), "Not found: user");
749 }
750
751 #[test]
752 fn test_db_error_code() {
753 let err = DbError::query("test");
754 assert_eq!(err.error_code(), "DB001");
755
756 let err = DbError::PoolError(PoolError::Timeout);
757 assert_eq!(err.error_code(), "PL002");
758 }
759
760 #[test]
761 fn test_db_error_source() {
762 let err = DbError::PoolError(PoolError::Timeout);
763 assert!(err.source().is_some());
764 }
765
766 #[test]
767 fn test_db_error_contextual_source_chain() {
768 let root = DbError::QueryError("table not found".to_string());
771 let wrapped = root.with_context("fetching user");
772 let outer = wrapped.with_context("user_service.fetch");
773
774 let source1 = outer.source().expect("outer should have source");
776 assert!(source1.source().is_none());
778
779 let ctx_chain = outer.context().expect("outer should have context");
781 assert_eq!(ctx_chain.context, "user_service.fetch");
782 let inner_ctx = ctx_chain
783 .previous
784 .as_ref()
785 .expect("should have previous context");
786 assert_eq!(inner_ctx.context, "fetching user");
787 assert!(inner_ctx.previous.is_none());
788
789 let root_cause = outer.root_cause();
791 assert!(matches!(root_cause, DbError::QueryError(_)));
792 }
793
794 #[test]
795 fn test_pool_error() {
796 let err = PoolError::Timeout;
797 assert_eq!(format!("{}", err), "Connection acquire timeout");
798 assert_eq!(err.error_code(), "PL002");
799 }
800
801 #[test]
802 fn test_cache_error() {
803 let err = CacheError::NotFound("key".to_string());
804 assert_eq!(format!("{}", err), "Cache key not found: key");
805 assert_eq!(err.error_code(), "CH001");
806 }
807
808 #[test]
809 fn test_error_hook_set_and_trigger() {
810 use std::sync::atomic::{AtomicU32, Ordering};
811 let counter = Arc::new(AtomicU32::new(0));
812 let c = counter.clone();
813 set_error_hook(Arc::new(move |_err: &DbError| {
814 c.fetch_add(1, Ordering::SeqCst);
815 }));
816 let err = DbError::query("hook test");
817 trigger_error_hook(&err);
818 assert_eq!(counter.load(Ordering::SeqCst), 1);
819 }
820
821 #[test]
822 fn test_error_hook_no_hook_silent() {
823 let err = DbError::query("no hook");
825 trigger_error_hook(&err);
826 }
827
828 #[test]
831 fn test_http_status_bad_request() {
832 assert_eq!(DbError::InvalidInput("bad".into()).http_status(), 400);
833 assert_eq!(DbError::Validation("fail".into()).http_status(), 400);
834 assert_eq!(DbError::ConfigError("cfg".into()).http_status(), 400);
835 }
836
837 #[test]
838 fn test_http_status_not_found() {
839 assert_eq!(DbError::NotFound("user".into()).http_status(), 404);
840 }
841
842 #[test]
843 fn test_http_status_conflict() {
844 assert_eq!(DbError::AlreadyExists("x".into()).http_status(), 409);
845 assert_eq!(DbError::ConstraintViolation("c".into()).http_status(), 409);
846 assert_eq!(DbError::UniqueViolation("u".into()).http_status(), 409);
847 assert_eq!(DbError::ForeignKeyViolation("f".into()).http_status(), 409);
848 assert_eq!(DbError::NullValue("n".into()).http_status(), 409);
849 }
850
851 #[test]
852 fn test_http_status_unprocessable() {
853 assert_eq!(DbError::SerdeError("s".into()).http_status(), 422);
854 }
855
856 #[test]
857 fn test_http_status_internal_server_error() {
858 assert_eq!(DbError::QueryError("q".into()).http_status(), 500);
859 assert_eq!(DbError::Internal("i".into()).http_status(), 500);
860 assert_eq!(DbError::Hook("h".into()).http_status(), 500);
861 assert_eq!(DbError::MigrationError("m".into()).http_status(), 500);
862 assert_eq!(DbError::IoError("io".into()).http_status(), 500);
863 assert_eq!(DbError::TxError(TxError::NotStarted).http_status(), 500);
864 assert_eq!(DbError::TenantError("t".into()).http_status(), 500);
865 }
866
867 #[test]
868 fn test_http_status_not_implemented() {
869 assert_eq!(DbError::Unsupported("feat".into()).http_status(), 501);
870 }
871
872 #[test]
873 fn test_http_status_bad_gateway() {
874 assert_eq!(DbError::ConnectionError("c".into()).http_status(), 502);
875 assert_eq!(DbError::ConnectionRefused("r".into()).http_status(), 502);
876 }
877
878 #[test]
879 fn test_http_status_service_unavailable() {
880 assert_eq!(DbError::PoolError(PoolError::Exhausted).http_status(), 503);
881 assert_eq!(DbError::PoolError(PoolError::Closed).http_status(), 503);
882 assert_eq!(
883 DbError::PoolError(PoolError::ConnectionFailed("f".into())).http_status(),
884 503
885 );
886 assert_eq!(
887 DbError::CacheError(CacheError::Internal("e".into())).http_status(),
888 503
889 );
890 }
891
892 #[test]
893 fn test_http_status_gateway_timeout() {
894 assert_eq!(DbError::ConnectionTimeout("t".into()).http_status(), 504);
895 assert_eq!(DbError::PoolError(PoolError::Timeout).http_status(), 504);
896 }
897
898 #[test]
901 fn test_grpc_status_invalid_argument() {
902 assert_eq!(DbError::InvalidInput("bad".into()).grpc_status_code(), 3);
903 assert_eq!(DbError::Validation("fail".into()).grpc_status_code(), 3);
904 assert_eq!(DbError::ConfigError("cfg".into()).grpc_status_code(), 3);
905 }
906
907 #[test]
908 fn test_grpc_status_deadline_exceeded() {
909 assert_eq!(DbError::ConnectionTimeout("t".into()).grpc_status_code(), 4);
910 assert_eq!(DbError::PoolError(PoolError::Timeout).grpc_status_code(), 4);
911 }
912
913 #[test]
914 fn test_grpc_status_not_found() {
915 assert_eq!(DbError::NotFound("user".into()).grpc_status_code(), 5);
916 }
917
918 #[test]
919 fn test_grpc_status_already_exists() {
920 assert_eq!(DbError::AlreadyExists("x".into()).grpc_status_code(), 6);
921 assert_eq!(DbError::UniqueViolation("u".into()).grpc_status_code(), 6);
922 }
923
924 #[test]
925 fn test_grpc_status_permission_denied() {
926 assert_eq!(DbError::TenantError("t".into()).grpc_status_code(), 7);
927 }
928
929 #[test]
930 fn test_grpc_status_resource_exhausted() {
931 assert_eq!(
932 DbError::PoolError(PoolError::Exhausted).grpc_status_code(),
933 8
934 );
935 assert_eq!(DbError::PoolError(PoolError::Closed).grpc_status_code(), 8);
936 assert_eq!(
937 DbError::CacheError(CacheError::Internal("e".into())).grpc_status_code(),
938 8
939 );
940 }
941
942 #[test]
943 fn test_grpc_status_failed_precondition() {
944 assert_eq!(
945 DbError::ConstraintViolation("c".into()).grpc_status_code(),
946 9
947 );
948 assert_eq!(
949 DbError::ForeignKeyViolation("f".into()).grpc_status_code(),
950 9
951 );
952 assert_eq!(DbError::NullValue("n".into()).grpc_status_code(), 9);
953 assert_eq!(DbError::TxError(TxError::NotStarted).grpc_status_code(), 9);
954 }
955
956 #[test]
957 fn test_grpc_status_unimplemented() {
958 assert_eq!(DbError::Unsupported("feat".into()).grpc_status_code(), 12);
959 }
960
961 #[test]
962 fn test_grpc_status_internal() {
963 assert_eq!(DbError::SerdeError("s".into()).grpc_status_code(), 13);
964 }
965
966 #[test]
967 fn test_grpc_status_unavailable() {
968 assert_eq!(DbError::ConnectionError("c".into()).grpc_status_code(), 14);
969 assert_eq!(
970 DbError::ConnectionRefused("r".into()).grpc_status_code(),
971 14
972 );
973 assert_eq!(
974 DbError::PoolError(PoolError::ConnectionFailed("f".into())).grpc_status_code(),
975 14
976 );
977 }
978
979 #[test]
980 fn test_grpc_status_unknown() {
981 assert_eq!(DbError::QueryError("q".into()).grpc_status_code(), 2);
982 assert_eq!(DbError::Internal("i".into()).grpc_status_code(), 2);
983 assert_eq!(DbError::Hook("h".into()).grpc_status_code(), 2);
984 assert_eq!(DbError::MigrationError("m".into()).grpc_status_code(), 2);
985 assert_eq!(DbError::IoError("io".into()).grpc_status_code(), 2);
986 assert_eq!(
988 DbError::PoolError(PoolError::AlreadyAcquired).grpc_status_code(),
989 2
990 );
991 assert_eq!(
992 DbError::PoolError(PoolError::NotAcquired).grpc_status_code(),
993 2
994 );
995 assert_eq!(
996 DbError::PoolError(PoolError::InvalidConfig("x".into())).grpc_status_code(),
997 2
998 );
999 assert_eq!(
1000 DbError::PoolError(PoolError::Internal("y".into())).grpc_status_code(),
1001 2
1002 );
1003 }
1004}