Skip to main content

sentinel_driver/
error.rs

1use std::fmt;
2
3/// Result type alias for sentinel-driver operations.
4pub type Result<T> = std::result::Result<T, Error>;
5
6/// All possible errors from sentinel-driver.
7#[derive(Debug, thiserror::Error)]
8pub enum Error {
9    /// I/O error from TCP/TLS stream.
10    #[error("io error: {0}")]
11    Io(#[from] std::io::Error),
12
13    /// PostgreSQL protocol error (unexpected message, malformed packet, etc.).
14    #[error("protocol error: {0}")]
15    Protocol(String),
16
17    /// Error returned by the PostgreSQL server.
18    #[error("{0}")]
19    Server(Box<ServerError>),
20
21    /// Authentication failure.
22    #[error("authentication failed: {0}")]
23    Auth(String),
24
25    /// TLS/SSL negotiation error.
26    #[error("tls error: {0}")]
27    Tls(String),
28
29    /// Connection pool error.
30    #[error("pool error: {0}")]
31    Pool(String),
32
33    /// Invalid configuration.
34    #[error("config error: {0}")]
35    Config(String),
36
37    /// Type encoding error (Rust → PG).
38    #[error("encode error: {0}")]
39    Encode(String),
40
41    /// Type decoding error (PG → Rust).
42    #[error("decode error: {0}")]
43    Decode(String),
44
45    /// Column not found by name.
46    #[error("column not found: {0}")]
47    ColumnNotFound(String),
48
49    /// Column index out of bounds.
50    #[error("column index {index} out of bounds (row has {count} columns)")]
51    ColumnIndex { index: usize, count: usize },
52
53    /// Unexpected null value.
54    #[error("unexpected null in column {0}")]
55    UnexpectedNull(usize),
56
57    /// Timeout (connect, query, pool checkout).
58    #[error("timeout: {0}")]
59    Timeout(String),
60
61    /// Connection is closed or broken.
62    #[error("connection closed")]
63    ConnectionClosed,
64
65    /// COPY protocol error.
66    #[error("copy error: {0}")]
67    Copy(String),
68
69    /// Transaction already completed (committed or rolled back).
70    #[error("transaction already completed")]
71    TransactionCompleted,
72}
73
74/// PostgreSQL server error details.
75#[derive(Debug, Clone)]
76pub struct ServerError {
77    pub severity: String,
78    pub code: String,
79    pub message: String,
80    pub detail: Option<String>,
81    pub hint: Option<String>,
82    pub position: Option<u32>,
83}
84
85impl fmt::Display for ServerError {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(
88            f,
89            "{}: {} (SQLSTATE {})",
90            self.severity, self.message, self.code
91        )
92    }
93}
94
95impl Error {
96    /// Returns the SQLSTATE code if this is a server error.
97    pub fn code(&self) -> Option<&str> {
98        match self {
99            Error::Server(e) => Some(&e.code),
100            _ => None,
101        }
102    }
103
104    /// Returns the server error details if this is a server error.
105    pub fn server_error(&self) -> Option<&ServerError> {
106        match self {
107            Error::Server(e) => Some(e),
108            _ => None,
109        }
110    }
111
112    /// Returns `true` if this error represents a unique violation (SQLSTATE 23505).
113    pub fn is_unique_violation(&self) -> bool {
114        self.code() == Some("23505")
115    }
116
117    /// Returns `true` if this error represents a foreign key violation (SQLSTATE 23503).
118    pub fn is_foreign_key_violation(&self) -> bool {
119        self.code() == Some("23503")
120    }
121
122    /// Returns `true` if the connection should be considered broken.
123    pub fn is_fatal(&self) -> bool {
124        matches!(self, Error::Io(_) | Error::ConnectionClosed | Error::Tls(_))
125    }
126}
127
128impl Error {
129    /// Create a protocol error from a string.
130    pub(crate) fn protocol(msg: impl Into<String>) -> Self {
131        Error::Protocol(msg.into())
132    }
133
134    /// Create a server error from ErrorResponse fields.
135    pub(crate) fn server(
136        severity: String,
137        code: String,
138        message: String,
139        detail: Option<String>,
140        hint: Option<String>,
141        position: Option<u32>,
142    ) -> Self {
143        Error::Server(Box::new(ServerError {
144            severity,
145            code,
146            message,
147            detail,
148            hint,
149            position,
150        }))
151    }
152}
153
154/// Severity level from PostgreSQL ErrorResponse.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum Severity {
157    Error,
158    Fatal,
159    Panic,
160    Warning,
161    Notice,
162    Debug,
163    Info,
164    Log,
165}
166
167impl fmt::Display for Severity {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            Severity::Error => write!(f, "ERROR"),
171            Severity::Fatal => write!(f, "FATAL"),
172            Severity::Panic => write!(f, "PANIC"),
173            Severity::Warning => write!(f, "WARNING"),
174            Severity::Notice => write!(f, "NOTICE"),
175            Severity::Debug => write!(f, "DEBUG"),
176            Severity::Info => write!(f, "INFO"),
177            Severity::Log => write!(f, "LOG"),
178        }
179    }
180}