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 returned by `sentinel-driver`.
7///
8/// # Stability contract
9///
10/// This enum is `#[non_exhaustive]` as of v1.1.0. New variants may be
11/// added in any future minor release without a major version bump, in
12/// line with the additive-only stability policy in `GOVERNANCE.md`.
13///
14/// What this means for downstream code:
15///
16/// - `?` propagation and `From`/`Into` conversion are unaffected.
17/// - Manual exhaustive `match` arms must include a wildcard `_ =>` arm
18///   to keep compiling against future minor releases. This is the only
19///   migration required by the v1.0 → v1.1 transition.
20///
21/// ```ignore
22/// // OK — has a wildcard arm.
23/// match err {
24///     Error::ConnectionClosed => /* ... */,
25///     Error::TransactionCompleted => /* ... */,
26///     _ => /* ... */,
27/// }
28/// ```
29#[derive(Debug, thiserror::Error)]
30#[non_exhaustive]
31pub enum Error {
32    /// I/O error from TCP/TLS stream.
33    #[error("io error: {0}")]
34    Io(#[from] std::io::Error),
35
36    /// PostgreSQL protocol error (unexpected message, malformed packet, etc.).
37    #[error("protocol error: {0}")]
38    Protocol(String),
39
40    /// Error returned by the PostgreSQL server.
41    #[error("{0}")]
42    Server(Box<ServerError>),
43
44    /// Authentication failure.
45    #[error("authentication failed: {0}")]
46    Auth(String),
47
48    /// TLS/SSL negotiation error.
49    #[error("tls error: {0}")]
50    Tls(String),
51
52    /// Connection pool error.
53    #[error("pool error: {0}")]
54    Pool(String),
55
56    /// Invalid configuration.
57    #[error("config error: {0}")]
58    Config(String),
59
60    /// Type encoding error (Rust → PG).
61    #[error("encode error: {0}")]
62    Encode(String),
63
64    /// Type decoding error (PG → Rust).
65    #[error("decode error: {0}")]
66    Decode(String),
67
68    /// Column not found by name.
69    #[error("column not found: {0}")]
70    ColumnNotFound(String),
71
72    /// Column index out of bounds.
73    #[error("column index {index} out of bounds (row has {count} columns)")]
74    ColumnIndex { index: usize, count: usize },
75
76    /// Unexpected null value.
77    #[error("unexpected null in column {0}")]
78    UnexpectedNull(usize),
79
80    /// Timeout (connect, query, pool checkout).
81    #[error("timeout: {0}")]
82    Timeout(String),
83
84    /// Connection is closed or broken.
85    #[error("connection closed")]
86    ConnectionClosed,
87
88    /// COPY protocol error.
89    #[error("copy error: {0}")]
90    Copy(String),
91
92    /// Transaction already completed (committed or rolled back).
93    #[error("transaction already completed")]
94    TransactionCompleted,
95
96    /// All configured hosts failed to connect.
97    #[error("all hosts failed: {0}")]
98    AllHostsFailed(String),
99
100    /// Connected server does not match required session attributes.
101    #[error("wrong session attributes: {0}")]
102    WrongSessionAttrs(String),
103}
104
105/// PostgreSQL server error details.
106#[derive(Debug, Clone)]
107#[non_exhaustive]
108pub struct ServerError {
109    pub severity: String,
110    pub code: String,
111    pub message: String,
112    pub detail: Option<String>,
113    pub hint: Option<String>,
114    pub position: Option<u32>,
115}
116
117impl fmt::Display for ServerError {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(
120            f,
121            "{}: {} (SQLSTATE {})",
122            self.severity, self.message, self.code
123        )
124    }
125}
126
127impl Error {
128    /// Returns the SQLSTATE code if this is a server error.
129    pub fn code(&self) -> Option<&str> {
130        match self {
131            Error::Server(e) => Some(&e.code),
132            _ => None,
133        }
134    }
135
136    /// Returns the server error details if this is a server error.
137    pub fn server_error(&self) -> Option<&ServerError> {
138        match self {
139            Error::Server(e) => Some(e),
140            _ => None,
141        }
142    }
143
144    /// Returns `true` if this error represents a unique violation (SQLSTATE 23505).
145    pub fn is_unique_violation(&self) -> bool {
146        self.code() == Some("23505")
147    }
148
149    /// Returns `true` if this error represents a foreign key violation (SQLSTATE 23503).
150    pub fn is_foreign_key_violation(&self) -> bool {
151        self.code() == Some("23503")
152    }
153
154    /// Returns `true` if the connection should be considered broken.
155    pub fn is_fatal(&self) -> bool {
156        matches!(self, Error::Io(_) | Error::ConnectionClosed | Error::Tls(_))
157    }
158}
159
160impl Error {
161    /// Create a protocol error from a string.
162    pub(crate) fn protocol(msg: impl Into<String>) -> Self {
163        Error::Protocol(msg.into())
164    }
165
166    /// Create a server error from ErrorResponse fields.
167    pub(crate) fn server(
168        severity: String,
169        code: String,
170        message: String,
171        detail: Option<String>,
172        hint: Option<String>,
173        position: Option<u32>,
174    ) -> Self {
175        Error::Server(Box::new(ServerError {
176            severity,
177            code,
178            message,
179            detail,
180            hint,
181            position,
182        }))
183    }
184}
185
186/// Severity level from PostgreSQL ErrorResponse.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188#[non_exhaustive]
189pub enum Severity {
190    Error,
191    Fatal,
192    Panic,
193    Warning,
194    Notice,
195    Debug,
196    Info,
197    Log,
198}
199
200impl fmt::Display for Severity {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        match self {
203            Severity::Error => write!(f, "ERROR"),
204            Severity::Fatal => write!(f, "FATAL"),
205            Severity::Panic => write!(f, "PANIC"),
206            Severity::Warning => write!(f, "WARNING"),
207            Severity::Notice => write!(f, "NOTICE"),
208            Severity::Debug => write!(f, "DEBUG"),
209            Severity::Info => write!(f, "INFO"),
210            Severity::Log => write!(f, "LOG"),
211        }
212    }
213}