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