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)]
48#[non_exhaustive]
49pub enum DbError {
50 QueryError(String),
52
53 ConnectionError(String),
55
56 ConnectionRefused(String),
58
59 ConnectionTimeout(String),
61
62 PoolError(PoolError),
64
65 CacheError(CacheError),
67
68 TxError(TxError),
70
71 MigrationError(String),
73
74 Unsupported(String),
76
77 ConfigError(String),
79
80 SerdeError(String),
82
83 NotFound(String),
85
86 AlreadyExists(String),
88
89 ConstraintViolation(String),
91
92 UniqueViolation(String),
94
95 ForeignKeyViolation(String),
97
98 NullValue(String),
100
101 InvalidInput(String),
103
104 Internal(String),
106
107 IoError(String),
109
110 Hook(String),
112
113 TenantError(String),
115
116 Validation(String),
118
119 Contextual {
124 source: Box<DbError>,
126 context: ErrorContext,
128 },
129
130 Extension {
137 ext_name: &'static str,
139 message: String,
141 },
142}
143
144#[derive(Debug, Clone)]
149pub struct ErrorContext {
150 pub context: String,
152 pub span: Option<String>,
154 pub previous: Option<Box<ErrorContext>>,
156}
157
158impl ErrorContext {
159 pub fn new(context: impl Into<String>) -> Self {
161 Self {
162 context: context.into(),
163 span: None,
164 previous: None,
165 }
166 }
167
168 pub fn with_previous(mut self, prev: ErrorContext) -> Self {
170 self.previous = Some(Box::new(prev));
171 self
172 }
173
174 pub fn with_span(mut self, span: impl Into<String>) -> Self {
176 self.span = Some(span.into());
177 self
178 }
179
180 pub fn iter(&self) -> impl Iterator<Item = &ErrorContext> {
182 let mut current = Some(self);
183 std::iter::from_fn(move || {
184 let node = current?;
185 let result = node;
186 current = node.previous.as_deref();
187 Some(result)
188 })
189 }
190
191 pub fn format_chain(&self) -> String {
193 self.iter()
194 .enumerate()
195 .map(|(i, ctx)| {
196 if let Some(ref span) = ctx.span {
197 format!(" [{}] {} (span: {})", i, ctx.context, span)
198 } else {
199 format!(" [{}] {}", i, ctx.context)
200 }
201 })
202 .collect::<Vec<_>>()
203 .join("\n")
204 }
205}
206
207impl DbError {
208 pub fn query(s: impl Into<String>) -> Self {
210 DbError::QueryError(s.into())
211 }
212
213 pub fn connection(s: impl Into<String>) -> Self {
215 DbError::ConnectionError(s.into())
216 }
217
218 pub fn not_found(s: impl Into<String>) -> Self {
220 DbError::NotFound(s.into())
221 }
222
223 pub fn with_context(self, context: impl Into<String>) -> Self {
239 let new_ctx = ErrorContext::new(context);
240 match self {
242 DbError::Contextual {
243 source,
244 context: existing_ctx,
245 } => {
246 let new_ctx = new_ctx.with_previous(existing_ctx);
247 DbError::Contextual {
248 source,
249 context: new_ctx,
250 }
251 }
252 other => DbError::Contextual {
253 source: Box::new(other),
254 context: new_ctx,
255 },
256 }
257 }
258
259 pub fn with_context_in_span(self, context: impl Into<String>, span: impl Into<String>) -> Self {
261 let new_ctx = ErrorContext::new(context).with_span(span);
262 match self {
263 DbError::Contextual {
264 source,
265 context: existing_ctx,
266 } => {
267 let new_ctx = new_ctx.with_previous(existing_ctx);
268 DbError::Contextual {
269 source,
270 context: new_ctx,
271 }
272 }
273 other => DbError::Contextual {
274 source: Box::new(other),
275 context: new_ctx,
276 },
277 }
278 }
279
280 pub fn context(&self) -> Option<&ErrorContext> {
282 match self {
283 DbError::Contextual { context, .. } => Some(context),
284 _ => None,
285 }
286 }
287
288 pub fn format_context_chain(&self) -> String {
290 match self {
291 DbError::Contextual { context, .. } => context.format_chain(),
292 _ => String::new(),
293 }
294 }
295
296 pub fn root_cause(&self) -> &DbError {
298 match self {
299 DbError::Contextual { source, .. } => source.root_cause(),
300 other => other,
301 }
302 }
303
304 pub fn is_retryable(&self) -> bool {
306 self.root_cause_is_retryable()
307 }
308
309 fn root_cause_is_retryable(&self) -> bool {
311 match self {
312 DbError::Contextual { source, .. } => source.root_cause_is_retryable(),
313 DbError::ConnectionError(_)
314 | DbError::ConnectionTimeout(_)
315 | DbError::PoolError(PoolError::Timeout) => true,
316 _ => false,
317 }
318 }
319
320 pub fn error_code(&self) -> &'static str {
322 match self {
323 DbError::Contextual { source, .. } => source.error_code(),
324 DbError::QueryError(_) => "DB001",
325 DbError::ConnectionError(_) => "DB002",
326 DbError::ConnectionRefused(_) => "DB003",
327 DbError::ConnectionTimeout(_) => "DB004",
328 DbError::PoolError(e) => e.error_code(),
329 DbError::CacheError(e) => e.error_code(),
330 DbError::TxError(_) => "DB007",
331 DbError::MigrationError(_) => "DB008",
332 DbError::Unsupported(_) => "DB009",
333 DbError::ConfigError(_) => "DB010",
334 DbError::SerdeError(_) => "DB011",
335 DbError::NotFound(_) => "DB012",
336 DbError::AlreadyExists(_) => "DB013",
337 DbError::ConstraintViolation(_) => "DB014",
338 DbError::UniqueViolation(_) => "DB022",
339 DbError::ForeignKeyViolation(_) => "DB023",
340 DbError::NullValue(_) => "DB015",
341 DbError::InvalidInput(_) => "DB016",
342 DbError::Internal(_) => "DB017",
343 DbError::IoError(_) => "DB018",
344 DbError::Hook(_) => "DB019",
345 DbError::TenantError(_) => "DB020",
346 DbError::Validation(_) => "DB021",
347 DbError::Extension { .. } => "DB024",
348 }
349 }
350
351 pub fn http_status(&self) -> u16 {
364 match self {
365 DbError::Contextual { source, .. } => source.http_status(),
366 DbError::InvalidInput(_) | DbError::Validation(_) | DbError::ConfigError(_) => 400,
367 DbError::NotFound(_) => 404,
368 DbError::AlreadyExists(_)
369 | DbError::ConstraintViolation(_)
370 | DbError::UniqueViolation(_)
371 | DbError::ForeignKeyViolation(_)
372 | DbError::NullValue(_) => 409,
373 DbError::SerdeError(_) => 422,
374 DbError::Unsupported(_) => 501,
375 DbError::ConnectionError(_) | DbError::ConnectionRefused(_) => 502,
376 DbError::ConnectionTimeout(_) => 504,
377 DbError::PoolError(e) => match e {
378 PoolError::Timeout => 504,
379 PoolError::Exhausted | PoolError::Closed | PoolError::ConnectionFailed(_) => 503,
380 _ => 500,
381 },
382 DbError::CacheError(_) => 503,
383 _ => 500,
385 }
386 }
387
388 pub fn grpc_status_code(&self) -> u32 {
404 match self {
405 DbError::Contextual { source, .. } => source.grpc_status_code(),
406 DbError::InvalidInput(_) | DbError::Validation(_) | DbError::ConfigError(_) => 3,
407 DbError::ConnectionTimeout(_) => 4,
408 DbError::PoolError(PoolError::Timeout) => 4,
409 DbError::NotFound(_) => 5,
410 DbError::AlreadyExists(_) | DbError::UniqueViolation(_) => 6,
411 DbError::TenantError(_) => 7,
412 DbError::PoolError(PoolError::Exhausted) | DbError::PoolError(PoolError::Closed) => 8,
413 DbError::CacheError(_) => 8,
414 DbError::ConstraintViolation(_)
415 | DbError::ForeignKeyViolation(_)
416 | DbError::NullValue(_)
417 | DbError::TxError(_) => 9,
418 DbError::Unsupported(_) => 12,
419 DbError::SerdeError(_) => 13,
420 DbError::ConnectionError(_)
421 | DbError::ConnectionRefused(_)
422 | DbError::PoolError(PoolError::ConnectionFailed(_)) => 14,
423 _ => 2,
425 }
426 }
427}
428
429impl fmt::Display for DbError {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 match self {
432 DbError::QueryError(s) => write!(f, "Query error: {}", s),
433 DbError::ConnectionError(s) => write!(f, "Connection error: {}", s),
434 DbError::ConnectionRefused(s) => write!(f, "Connection refused: {}", s),
435 DbError::ConnectionTimeout(s) => write!(f, "Connection timeout: {}", s),
436 DbError::PoolError(e) => write!(f, "Pool error: {}", e),
437 DbError::CacheError(e) => write!(f, "Cache error: {}", e),
438 DbError::TxError(e) => write!(f, "Transaction error: {}", e),
439 DbError::MigrationError(s) => write!(f, "Migration error: {}", s),
440 DbError::Unsupported(s) => write!(f, "Unsupported: {}", s),
441 DbError::ConfigError(s) => write!(f, "Configuration error: {}", s),
442 DbError::SerdeError(s) => write!(f, "Serialization error: {}", s),
443 DbError::NotFound(s) => write!(f, "Not found: {}", s),
444 DbError::AlreadyExists(s) => write!(f, "Already exists: {}", s),
445 DbError::ConstraintViolation(s) => write!(f, "Constraint violation: {}", s),
446 DbError::UniqueViolation(s) => write!(f, "Unique constraint violation: {}", s),
447 DbError::ForeignKeyViolation(s) => write!(f, "Foreign key constraint violation: {}", s),
448 DbError::NullValue(s) => write!(f, "Null value: {}", s),
449 DbError::InvalidInput(s) => write!(f, "Invalid input: {}", s),
450 DbError::Internal(s) => write!(f, "Internal error: {}", s),
451 DbError::IoError(s) => write!(f, "IO error: {}", s),
452 DbError::Hook(s) => write!(f, "Hook error: {}", s),
453 DbError::TenantError(s) => write!(f, "Tenant error: {}", s),
454 DbError::Validation(s) => write!(f, "Validation error: {}", s),
455 DbError::Contextual {
456 context, source, ..
457 } => write!(f, "{}: {}", context.context, source),
458 DbError::Extension { ext_name, message } => {
459 write!(f, "[{}] extension error: {}", ext_name, message)
460 }
461 }
462 }
463}
464
465impl Error for DbError {
466 fn source(&self) -> Option<&(dyn Error + 'static)> {
467 match self {
468 DbError::PoolError(e) => Some(e),
469 DbError::CacheError(e) => Some(e),
470 DbError::TxError(e) => Some(e),
471 DbError::Contextual { source, .. } => Some(source.as_ref()),
474 _ => None,
475 }
476 }
477}
478
479impl From<io::Error> for DbError {
480 fn from(err: io::Error) -> Self {
481 DbError::IoError(err.to_string())
482 }
483}
484
485impl From<serde_json::Error> for DbError {
486 fn from(err: serde_json::Error) -> Self {
487 DbError::SerdeError(err.to_string())
488 }
489}
490
491impl From<std::num::TryFromIntError> for DbError {
492 fn from(err: std::num::TryFromIntError) -> Self {
493 DbError::Internal(err.to_string())
494 }
495}
496
497impl From<std::string::FromUtf8Error> for DbError {
498 fn from(err: std::string::FromUtf8Error) -> Self {
499 DbError::Internal(err.to_string())
500 }
501}
502
503impl<T> From<std::sync::PoisonError<T>> for DbError {
504 fn from(err: std::sync::PoisonError<T>) -> Self {
505 DbError::Internal(format!("RwLock/Mutex poisoned: {}", err))
506 }
507}
508
509#[derive(Debug)]
511pub enum PoolError {
512 Exhausted,
514
515 Timeout,
517
518 AlreadyAcquired,
520
521 NotAcquired,
523
524 InvalidConfig(String),
526
527 Internal(String),
529
530 Closed,
532
533 ConnectionFailed(String),
535
536 CircuitOpen,
542
543 RateLimited { remaining: u64, reset_at: i64 },
548}
549
550impl PoolError {
551 pub fn error_code(&self) -> &'static str {
552 match self {
553 PoolError::Exhausted => "PL001",
554 PoolError::Timeout => "PL002",
555 PoolError::AlreadyAcquired => "PL003",
556 PoolError::NotAcquired => "PL004",
557 PoolError::InvalidConfig(_) => "PL005",
558 PoolError::Internal(_) => "PL006",
559 PoolError::Closed => "PL007",
560 PoolError::ConnectionFailed(_) => "PL008",
561 PoolError::CircuitOpen => "PL009",
562 PoolError::RateLimited { .. } => "PL010",
563 }
564 }
565}
566
567impl fmt::Display for PoolError {
568 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569 match self {
570 PoolError::Exhausted => write!(f, "Connection pool exhausted"),
571 PoolError::Timeout => write!(f, "Connection acquire timeout"),
572 PoolError::AlreadyAcquired => write!(f, "Connection already acquired"),
573 PoolError::NotAcquired => write!(f, "Connection not acquired"),
574 PoolError::InvalidConfig(s) => write!(f, "Invalid pool config: {}", s),
575 PoolError::Internal(s) => write!(f, "Internal pool error: {}", s),
576 PoolError::Closed => write!(f, "Connection pool closed"),
577 PoolError::ConnectionFailed(s) => write!(f, "Connection failed: {}", s),
578 PoolError::CircuitOpen => write!(f, "Circuit breaker open"),
579 PoolError::RateLimited {
580 remaining,
581 reset_at,
582 } => write!(
583 f,
584 "Rate limited (remaining: {}, reset_at: {})",
585 remaining, reset_at
586 ),
587 }
588 }
589}
590
591impl Error for PoolError {}
592
593#[derive(Debug, Clone)]
595pub enum CacheError {
596 NotFound(String),
598
599 SerializationError(String),
601
602 DeserializationError(String),
604
605 ConnectionError(String),
607
608 Timeout(String),
610
611 Internal(String),
613}
614
615impl CacheError {
616 pub fn error_code(&self) -> &'static str {
617 match self {
618 CacheError::NotFound(_) => "CH001",
619 CacheError::SerializationError(_) => "CH002",
620 CacheError::DeserializationError(_) => "CH003",
621 CacheError::ConnectionError(_) => "CH004",
622 CacheError::Timeout(_) => "CH005",
623 CacheError::Internal(_) => "CH006",
624 }
625 }
626}
627
628impl fmt::Display for CacheError {
629 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
630 match self {
631 CacheError::NotFound(s) => write!(f, "Cache key not found: {}", s),
632 CacheError::SerializationError(s) => write!(f, "Cache serialization error: {}", s),
633 CacheError::DeserializationError(s) => write!(f, "Cache deserialization error: {}", s),
634 CacheError::ConnectionError(s) => write!(f, "Cache connection error: {}", s),
635 CacheError::Timeout(s) => write!(f, "Cache timeout: {}", s),
636 CacheError::Internal(s) => write!(f, "Cache internal error: {}", s),
637 }
638 }
639}
640
641impl Error for CacheError {}
642
643impl<T> From<std::sync::PoisonError<T>> for CacheError {
644 fn from(err: std::sync::PoisonError<T>) -> Self {
645 CacheError::Internal(format!("RwLock poisoned: {}", err))
646 }
647}
648
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
654pub enum TransactionState {
655 #[default]
656 Active,
657 Committed,
658 RolledBack,
659}
660
661impl fmt::Display for TransactionState {
662 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
663 match self {
664 TransactionState::Active => write!(f, "Active"),
665 TransactionState::Committed => write!(f, "Committed"),
666 TransactionState::RolledBack => write!(f, "RolledBack"),
667 }
668 }
669}
670
671#[derive(Debug)]
673pub enum TxError {
674 NotStarted,
676
677 AlreadyStarted,
679
680 CommitFailed(String),
682
683 RollbackFailed(String),
685
686 SavepointError(String),
688
689 NestedNotSupported,
691
692 NotActive(TransactionState),
694
695 InvalidSavepointName(String),
697
698 ConnectionTaken,
700
701 MaxNestingDepthExceeded { current_depth: u32, max_depth: u32 },
705
706 DeadlockDetected { attempt: u32, max_attempts: u32 },
711}
712
713impl fmt::Display for TxError {
714 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
715 match self {
716 TxError::NotStarted => write!(f, "Transaction not started"),
717 TxError::AlreadyStarted => write!(f, "Transaction already started"),
718 TxError::CommitFailed(s) => write!(f, "Transaction commit failed: {}", s),
719 TxError::RollbackFailed(s) => write!(f, "Transaction rollback failed: {}", s),
720 TxError::SavepointError(s) => write!(f, "Savepoint error: {}", s),
721 TxError::NestedNotSupported => write!(f, "Nested transactions not supported"),
722 TxError::NotActive(state) => {
723 write!(f, "Transaction not active (current state: {})", state)
724 }
725 TxError::InvalidSavepointName(name) => {
726 write!(
727 f,
728 "Invalid savepoint name '{}': must be non-empty, start with a letter or underscore, and contain only ASCII alphanumeric or underscore",
729 name
730 )
731 }
732 TxError::ConnectionTaken => write!(f, "Transaction connection already taken"),
733 TxError::MaxNestingDepthExceeded {
734 current_depth,
735 max_depth,
736 } => write!(
737 f,
738 "Transaction nesting depth {} exceeds maximum allowed {}",
739 current_depth, max_depth
740 ),
741 TxError::DeadlockDetected {
742 attempt,
743 max_attempts,
744 } => write!(
745 f,
746 "Deadlock detected on attempt {} of {}",
747 attempt, max_attempts
748 ),
749 }
750 }
751}
752
753impl Error for TxError {
754 fn source(&self) -> Option<&(dyn Error + 'static)> {
755 None
757 }
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 #[test]
765 fn test_db_error_display() {
766 let err = DbError::query("test");
767 assert_eq!(format!("{}", err), "Query error: test");
768
769 let err = DbError::not_found("user");
770 assert_eq!(format!("{}", err), "Not found: user");
771 }
772
773 #[test]
774 fn test_db_error_code() {
775 let err = DbError::query("test");
776 assert_eq!(err.error_code(), "DB001");
777
778 let err = DbError::PoolError(PoolError::Timeout);
779 assert_eq!(err.error_code(), "PL002");
780 }
781
782 #[test]
783 fn test_db_error_source() {
784 let err = DbError::PoolError(PoolError::Timeout);
785 assert!(err.source().is_some());
786 }
787
788 #[test]
789 fn test_db_error_contextual_source_chain() {
790 let root = DbError::QueryError("table not found".to_string());
793 let wrapped = root.with_context("fetching user");
794 let outer = wrapped.with_context("user_service.fetch");
795
796 let source1 = outer.source().expect("outer should have source");
798 assert!(source1.source().is_none());
800
801 let ctx_chain = outer.context().expect("outer should have context");
803 assert_eq!(ctx_chain.context, "user_service.fetch");
804 let inner_ctx = ctx_chain
805 .previous
806 .as_ref()
807 .expect("should have previous context");
808 assert_eq!(inner_ctx.context, "fetching user");
809 assert!(inner_ctx.previous.is_none());
810
811 let root_cause = outer.root_cause();
813 assert!(matches!(root_cause, DbError::QueryError(_)));
814 }
815
816 #[test]
817 fn test_pool_error() {
818 let err = PoolError::Timeout;
819 assert_eq!(format!("{}", err), "Connection acquire timeout");
820 assert_eq!(err.error_code(), "PL002");
821 }
822
823 #[test]
824 fn test_cache_error() {
825 let err = CacheError::NotFound("key".to_string());
826 assert_eq!(format!("{}", err), "Cache key not found: key");
827 assert_eq!(err.error_code(), "CH001");
828 }
829
830 #[test]
831 fn test_error_hook_set_and_trigger() {
832 use std::sync::atomic::{AtomicU32, Ordering};
833 let counter = Arc::new(AtomicU32::new(0));
834 let c = counter.clone();
835 set_error_hook(Arc::new(move |_err: &DbError| {
836 c.fetch_add(1, Ordering::SeqCst);
837 }));
838 let err = DbError::query("hook test");
839 trigger_error_hook(&err);
840 assert_eq!(counter.load(Ordering::SeqCst), 1);
841 }
842
843 #[test]
844 fn test_error_hook_no_hook_silent() {
845 let err = DbError::query("no hook");
847 trigger_error_hook(&err);
848 }
849
850 #[test]
853 fn test_http_status_bad_request() {
854 assert_eq!(DbError::InvalidInput("bad".into()).http_status(), 400);
855 assert_eq!(DbError::Validation("fail".into()).http_status(), 400);
856 assert_eq!(DbError::ConfigError("cfg".into()).http_status(), 400);
857 }
858
859 #[test]
860 fn test_http_status_not_found() {
861 assert_eq!(DbError::NotFound("user".into()).http_status(), 404);
862 }
863
864 #[test]
865 fn test_http_status_conflict() {
866 assert_eq!(DbError::AlreadyExists("x".into()).http_status(), 409);
867 assert_eq!(DbError::ConstraintViolation("c".into()).http_status(), 409);
868 assert_eq!(DbError::UniqueViolation("u".into()).http_status(), 409);
869 assert_eq!(DbError::ForeignKeyViolation("f".into()).http_status(), 409);
870 assert_eq!(DbError::NullValue("n".into()).http_status(), 409);
871 }
872
873 #[test]
874 fn test_http_status_unprocessable() {
875 assert_eq!(DbError::SerdeError("s".into()).http_status(), 422);
876 }
877
878 #[test]
879 fn test_http_status_internal_server_error() {
880 assert_eq!(DbError::QueryError("q".into()).http_status(), 500);
881 assert_eq!(DbError::Internal("i".into()).http_status(), 500);
882 assert_eq!(DbError::Hook("h".into()).http_status(), 500);
883 assert_eq!(DbError::MigrationError("m".into()).http_status(), 500);
884 assert_eq!(DbError::IoError("io".into()).http_status(), 500);
885 assert_eq!(DbError::TxError(TxError::NotStarted).http_status(), 500);
886 assert_eq!(DbError::TenantError("t".into()).http_status(), 500);
887 }
888
889 #[test]
890 fn test_http_status_not_implemented() {
891 assert_eq!(DbError::Unsupported("feat".into()).http_status(), 501);
892 }
893
894 #[test]
895 fn test_http_status_bad_gateway() {
896 assert_eq!(DbError::ConnectionError("c".into()).http_status(), 502);
897 assert_eq!(DbError::ConnectionRefused("r".into()).http_status(), 502);
898 }
899
900 #[test]
901 fn test_http_status_service_unavailable() {
902 assert_eq!(DbError::PoolError(PoolError::Exhausted).http_status(), 503);
903 assert_eq!(DbError::PoolError(PoolError::Closed).http_status(), 503);
904 assert_eq!(
905 DbError::PoolError(PoolError::ConnectionFailed("f".into())).http_status(),
906 503
907 );
908 assert_eq!(
909 DbError::CacheError(CacheError::Internal("e".into())).http_status(),
910 503
911 );
912 }
913
914 #[test]
915 fn test_http_status_gateway_timeout() {
916 assert_eq!(DbError::ConnectionTimeout("t".into()).http_status(), 504);
917 assert_eq!(DbError::PoolError(PoolError::Timeout).http_status(), 504);
918 }
919
920 #[test]
923 fn test_grpc_status_invalid_argument() {
924 assert_eq!(DbError::InvalidInput("bad".into()).grpc_status_code(), 3);
925 assert_eq!(DbError::Validation("fail".into()).grpc_status_code(), 3);
926 assert_eq!(DbError::ConfigError("cfg".into()).grpc_status_code(), 3);
927 }
928
929 #[test]
930 fn test_grpc_status_deadline_exceeded() {
931 assert_eq!(DbError::ConnectionTimeout("t".into()).grpc_status_code(), 4);
932 assert_eq!(DbError::PoolError(PoolError::Timeout).grpc_status_code(), 4);
933 }
934
935 #[test]
936 fn test_grpc_status_not_found() {
937 assert_eq!(DbError::NotFound("user".into()).grpc_status_code(), 5);
938 }
939
940 #[test]
941 fn test_grpc_status_already_exists() {
942 assert_eq!(DbError::AlreadyExists("x".into()).grpc_status_code(), 6);
943 assert_eq!(DbError::UniqueViolation("u".into()).grpc_status_code(), 6);
944 }
945
946 #[test]
947 fn test_grpc_status_permission_denied() {
948 assert_eq!(DbError::TenantError("t".into()).grpc_status_code(), 7);
949 }
950
951 #[test]
952 fn test_grpc_status_resource_exhausted() {
953 assert_eq!(
954 DbError::PoolError(PoolError::Exhausted).grpc_status_code(),
955 8
956 );
957 assert_eq!(DbError::PoolError(PoolError::Closed).grpc_status_code(), 8);
958 assert_eq!(
959 DbError::CacheError(CacheError::Internal("e".into())).grpc_status_code(),
960 8
961 );
962 }
963
964 #[test]
965 fn test_grpc_status_failed_precondition() {
966 assert_eq!(
967 DbError::ConstraintViolation("c".into()).grpc_status_code(),
968 9
969 );
970 assert_eq!(
971 DbError::ForeignKeyViolation("f".into()).grpc_status_code(),
972 9
973 );
974 assert_eq!(DbError::NullValue("n".into()).grpc_status_code(), 9);
975 assert_eq!(DbError::TxError(TxError::NotStarted).grpc_status_code(), 9);
976 }
977
978 #[test]
979 fn test_grpc_status_unimplemented() {
980 assert_eq!(DbError::Unsupported("feat".into()).grpc_status_code(), 12);
981 }
982
983 #[test]
984 fn test_grpc_status_internal() {
985 assert_eq!(DbError::SerdeError("s".into()).grpc_status_code(), 13);
986 }
987
988 #[test]
989 fn test_grpc_status_unavailable() {
990 assert_eq!(DbError::ConnectionError("c".into()).grpc_status_code(), 14);
991 assert_eq!(
992 DbError::ConnectionRefused("r".into()).grpc_status_code(),
993 14
994 );
995 assert_eq!(
996 DbError::PoolError(PoolError::ConnectionFailed("f".into())).grpc_status_code(),
997 14
998 );
999 }
1000
1001 #[test]
1002 fn test_grpc_status_unknown() {
1003 assert_eq!(DbError::QueryError("q".into()).grpc_status_code(), 2);
1004 assert_eq!(DbError::Internal("i".into()).grpc_status_code(), 2);
1005 assert_eq!(DbError::Hook("h".into()).grpc_status_code(), 2);
1006 assert_eq!(DbError::MigrationError("m".into()).grpc_status_code(), 2);
1007 assert_eq!(DbError::IoError("io".into()).grpc_status_code(), 2);
1008 assert_eq!(
1010 DbError::PoolError(PoolError::AlreadyAcquired).grpc_status_code(),
1011 2
1012 );
1013 assert_eq!(
1014 DbError::PoolError(PoolError::NotAcquired).grpc_status_code(),
1015 2
1016 );
1017 assert_eq!(
1018 DbError::PoolError(PoolError::InvalidConfig("x".into())).grpc_status_code(),
1019 2
1020 );
1021 assert_eq!(
1022 DbError::PoolError(PoolError::Internal("y".into())).grpc_status_code(),
1023 2
1024 );
1025 }
1026}