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    /// All configured hosts failed to connect.
74    #[error("all hosts failed: {0}")]
75    AllHostsFailed(String),
76
77    /// Connected server does not match required session attributes.
78    #[error("wrong session attributes: {0}")]
79    WrongSessionAttrs(String),
80}
81
82/// PostgreSQL server error details.
83#[derive(Debug, Clone)]
84pub struct ServerError {
85    pub severity: String,
86    pub code: String,
87    pub message: String,
88    pub detail: Option<String>,
89    pub hint: Option<String>,
90    pub position: Option<u32>,
91}
92
93impl fmt::Display for ServerError {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(
96            f,
97            "{}: {} (SQLSTATE {})",
98            self.severity, self.message, self.code
99        )
100    }
101}
102
103impl Error {
104    /// Returns the SQLSTATE code if this is a server error.
105    pub fn code(&self) -> Option<&str> {
106        match self {
107            Error::Server(e) => Some(&e.code),
108            _ => None,
109        }
110    }
111
112    /// Returns the server error details if this is a server error.
113    pub fn server_error(&self) -> Option<&ServerError> {
114        match self {
115            Error::Server(e) => Some(e),
116            _ => None,
117        }
118    }
119
120    /// Returns `true` if this error represents a unique violation (SQLSTATE 23505).
121    pub fn is_unique_violation(&self) -> bool {
122        self.code() == Some("23505")
123    }
124
125    /// Returns `true` if this error represents a foreign key violation (SQLSTATE 23503).
126    pub fn is_foreign_key_violation(&self) -> bool {
127        self.code() == Some("23503")
128    }
129
130    /// Returns `true` if the connection should be considered broken.
131    pub fn is_fatal(&self) -> bool {
132        matches!(self, Error::Io(_) | Error::ConnectionClosed | Error::Tls(_))
133    }
134}
135
136impl Error {
137    /// Create a protocol error from a string.
138    pub(crate) fn protocol(msg: impl Into<String>) -> Self {
139        Error::Protocol(msg.into())
140    }
141
142    /// Create a server error from ErrorResponse fields.
143    pub(crate) fn server(
144        severity: String,
145        code: String,
146        message: String,
147        detail: Option<String>,
148        hint: Option<String>,
149        position: Option<u32>,
150    ) -> Self {
151        Error::Server(Box::new(ServerError {
152            severity,
153            code,
154            message,
155            detail,
156            hint,
157            position,
158        }))
159    }
160}
161
162/// Severity level from PostgreSQL ErrorResponse.
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum Severity {
165    Error,
166    Fatal,
167    Panic,
168    Warning,
169    Notice,
170    Debug,
171    Info,
172    Log,
173}
174
175impl fmt::Display for Severity {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match self {
178            Severity::Error => write!(f, "ERROR"),
179            Severity::Fatal => write!(f, "FATAL"),
180            Severity::Panic => write!(f, "PANIC"),
181            Severity::Warning => write!(f, "WARNING"),
182            Severity::Notice => write!(f, "NOTICE"),
183            Severity::Debug => write!(f, "DEBUG"),
184            Severity::Info => write!(f, "INFO"),
185            Severity::Log => write!(f, "LOG"),
186        }
187    }
188}