Skip to main content

wasi_pg_client/error/
mod.rs

1//! Error types for the PostgreSQL client.
2//!
3//! This module defines the [`PgError`] enum which represents all possible errors
4//! that can occur when using the PostgreSQL client, along with supporting types:
5//!
6//! - [`PgServerError`] — structured PostgreSQL server error with all ErrorResponse fields
7//! - [`TransportError`] — network/transport layer errors
8//! - [`sqlstate`] — SQLSTATE error code constants and helpers
9//! - [`retry`] — retry helpers for transient errors
10
11pub mod retry;
12pub mod server;
13pub mod sqlstate;
14
15use std::fmt;
16
17use crate::protocol::ProtocolError;
18use crate::types::Error as TypeConversionError;
19
20pub use crate::transport::TransportError;
21pub use server::PgServerError;
22
23// Re-export PoolError from pg-pool for structured pool errors
24// Note: This is a separate crate, so we use a string-based approach
25// to avoid a circular dependency. PoolError variants are embedded
26// as structured data via the PoolErrorVariant enum below.
27
28/// Structured pool error variants, mirroring `pg-pool::PoolError`.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum PoolErrorVariant {
32    Exhausted,
33    Closed,
34    CreateFailed(String),
35    ResetFailed(String),
36}
37
38impl std::fmt::Display for PoolErrorVariant {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        match self {
41            PoolErrorVariant::Exhausted => write!(f, "connection pool exhausted"),
42            PoolErrorVariant::Closed => write!(f, "connection pool is closed"),
43            PoolErrorVariant::CreateFailed(msg) => {
44                write!(f, "failed to create pool connection: {msg}")
45            }
46            PoolErrorVariant::ResetFailed(msg) => write!(f, "connection reset failed: {msg}"),
47        }
48    }
49}
50
51impl std::error::Error for PoolErrorVariant {}
52
53// ---------------------------------------------------------------------------
54// PgError
55// ---------------------------------------------------------------------------
56
57/// The main error type for the PostgreSQL client.
58///
59/// This enum distinguishes between different error categories:
60///
61/// - **Server errors** — PostgreSQL returned an `ErrorResponse` message with
62///   structured fields (SQLSTATE code, severity, detail, hint, etc.).
63/// - **Transport errors** — Network-level failures (connection refused, timeout,
64///   TLS handshake failure, etc.).
65/// - **Protocol errors** — Wire protocol violations (unexpected message, bad encoding).
66/// - **Authentication errors** — Failed authentication (wrong password, unsupported method).
67/// - **Type conversion errors** — Failed conversion between Rust and PostgreSQL types.
68/// - **Configuration errors** — Invalid connection string or parameters.
69/// - **Connection state errors** — Connection closed, wrong state for operation.
70/// - **Column/row errors** — Column not found, index out of bounds, unexpected NULL.
71/// - **Timeout** — Operation timed out.
72/// - **Pool errors** — Connection pool exhaustion or management errors.
73#[derive(Debug)]
74#[non_exhaustive]
75pub enum PgError {
76    /// Error returned by the PostgreSQL server (ErrorResponse).
77    ///
78    /// Contains all fields from the ErrorResponse message: severity, SQLSTATE
79    /// code, message, detail, hint, position, constraint, etc.
80    ///
81    /// Boxed to reduce the overall size of the enum since `PgServerError`
82    /// contains many `String` and `Option` fields.
83    Server(Box<PgServerError>),
84
85    /// Wire protocol violation.
86    Protocol(ProtocolError),
87
88    /// Network/transport error.
89    Transport(TransportError),
90
91    /// Authentication failure.
92    Auth(String),
93
94    /// Type conversion error.
95    TypeConversion(TypeConversionError),
96
97    /// Configuration error (invalid connection string, missing required field, etc.).
98    Config(String),
99
100    /// Connection is closed.
101    ConnectionClosed,
102
103    /// Unexpected NULL value in a column that was expected to be non-null.
104    UnexpectedNull { column: String },
105
106    /// Column not found in the result set.
107    ColumnNotFound { name: String },
108
109    /// Column index out of bounds.
110    ColumnIndexOutOfBounds { index: usize, count: usize },
111
112    /// Operation timed out.
113    Timeout,
114
115    /// Connection pool error.
116    Pool(PoolErrorVariant),
117
118    /// Invalid connection state for the requested operation.
119    InvalidState(String),
120
121    /// The operation is not supported.
122    Unsupported(String),
123
124    /// Operation was cancelled.
125    Cancelled,
126
127    /// I/O error.
128    Io(std::io::Error),
129
130    /// TLS error.
131    #[cfg(feature = "tls")]
132    Tls(rustls::Error),
133
134    /// Any other error not covered by the above variants.
135    Other(String),
136}
137
138impl std::fmt::Display for PgError {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            PgError::Server(e) => write!(f, "server error: {}", e),
142            PgError::Protocol(e) => write!(f, "protocol error: {}", e),
143            PgError::Transport(e) => write!(f, "transport error: {}", e),
144            PgError::Auth(msg) => write!(f, "authentication error: {}", msg),
145            PgError::TypeConversion(e) => write!(f, "type conversion error: {}", e),
146            PgError::Config(msg) => write!(f, "configuration error: {}", msg),
147            PgError::ConnectionClosed => write!(f, "connection closed"),
148            PgError::UnexpectedNull { column } => write!(f, "unexpected NULL in column {}", column),
149            PgError::ColumnNotFound { name } => write!(f, "column not found: {}", name),
150            PgError::ColumnIndexOutOfBounds { index, count } => {
151                write!(
152                    f,
153                    "column index {} out of bounds (have {} columns)",
154                    index, count
155                )
156            }
157            PgError::Timeout => write!(f, "operation timed out"),
158            PgError::Pool(e) => write!(f, "pool error: {e}"),
159            PgError::InvalidState(msg) => write!(f, "invalid connection state: {}", msg),
160            PgError::Unsupported(msg) => write!(f, "unsupported: {}", msg),
161            PgError::Cancelled => write!(f, "cancelled"),
162            PgError::Io(e) => write!(f, "I/O error: {}", e),
163            #[cfg(feature = "tls")]
164            PgError::Tls(e) => write!(f, "TLS error: {}", e),
165            PgError::Other(msg) => write!(f, "{}", msg),
166        }
167    }
168}
169
170impl std::error::Error for PgError {
171    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
172        match self {
173            PgError::Server(e) => Some(e.as_ref()),
174            PgError::Protocol(e) => Some(e),
175            PgError::Transport(e) => Some(e),
176            PgError::TypeConversion(e) => Some(e),
177            PgError::Io(e) => Some(e),
178            PgError::Pool(e) => Some(e),
179            #[cfg(feature = "tls")]
180            PgError::Tls(e) => Some(e),
181            _ => None,
182        }
183    }
184}
185
186// ---------------------------------------------------------------------------
187// From impls
188// ---------------------------------------------------------------------------
189
190impl From<PgServerError> for PgError {
191    fn from(e: PgServerError) -> Self {
192        PgError::Server(Box::new(e))
193    }
194}
195
196impl From<ProtocolError> for PgError {
197    fn from(e: ProtocolError) -> Self {
198        PgError::Protocol(e)
199    }
200}
201
202impl From<TransportError> for PgError {
203    fn from(e: TransportError) -> Self {
204        PgError::Transport(e)
205    }
206}
207
208impl From<TypeConversionError> for PgError {
209    fn from(e: TypeConversionError) -> Self {
210        PgError::TypeConversion(e)
211    }
212}
213
214impl From<std::io::Error> for PgError {
215    fn from(e: std::io::Error) -> Self {
216        PgError::Io(e)
217    }
218}
219
220#[cfg(feature = "tls")]
221impl From<rustls::Error> for PgError {
222    fn from(e: rustls::Error) -> Self {
223        PgError::Tls(e)
224    }
225}
226
227/// Backward-compatible alias for [`PgError`].
228pub type Error = PgError;
229
230// ---------------------------------------------------------------------------
231// PgError methods
232// ---------------------------------------------------------------------------
233
234impl PgError {
235    /// Check if the error indicates the connection is broken and cannot be
236    /// recovered without reconnecting.
237    pub fn is_connection_broken(&self) -> bool {
238        match self {
239            PgError::ConnectionClosed => true,
240            PgError::Transport(TransportError::ConnectionReset) => true,
241            PgError::Transport(TransportError::UnexpectedEof) => true,
242            PgError::Transport(TransportError::ConnectionRefused) => true,
243            PgError::Server(ref e) => {
244                e.is_connection_exception() || e.is_admin_shutdown() || e.is_crash_shutdown()
245            }
246            PgError::Io(ref e) => {
247                matches!(
248                    e.kind(),
249                    std::io::ErrorKind::ConnectionReset
250                        | std::io::ErrorKind::ConnectionAborted
251                        | std::io::ErrorKind::BrokenPipe
252                        | std::io::ErrorKind::UnexpectedEof
253                )
254            }
255            _ => false,
256        }
257    }
258
259    /// Check if this error is potentially retryable without reconnecting.
260    pub fn is_retryable(&self) -> bool {
261        match self {
262            PgError::Server(e) => e.is_serialization_failure() || e.is_deadlock_detected(),
263            PgError::Transport(TransportError::Timeout) => true,
264            PgError::Timeout => true,
265            _ => false,
266        }
267    }
268
269    /// Returns the SQLSTATE error code if this is a server error.
270    pub fn code(&self) -> Option<&str> {
271        match self {
272            PgError::Server(e) => Some(&e.code),
273            _ => None,
274        }
275    }
276
277    /// Add context to an error by wrapping it in a descriptive message.
278    pub fn context(self, msg: impl Into<String>) -> Self {
279        PgError::Other(format!("{}: {}", msg.into(), self))
280    }
281}
282
283/// A specialized `Result` type for client operations.
284pub type Result<T> = std::result::Result<T, PgError>;
285
286// ---------------------------------------------------------------------------
287// Conversion from auth::AuthError
288// ---------------------------------------------------------------------------
289
290impl From<crate::auth::AuthError> for PgError {
291    fn from(e: crate::auth::AuthError) -> Self {
292        match e {
293            crate::auth::AuthError::PasswordRequired => PgError::Auth("password required".into()),
294            crate::auth::AuthError::UnsupportedSaslMechanisms(mechs) => {
295                PgError::Auth(format!("unsupported SASL mechanisms: {mechs:?}"))
296            }
297            crate::auth::AuthError::Scram(msg) => PgError::Auth(format!("SCRAM error: {msg}")),
298            crate::auth::AuthError::InsecureAuthentication { method } => PgError::Auth(format!(
299                "refusing {method} authentication over an unencrypted transport"
300            )),
301            crate::auth::AuthError::ServerError(msg) => PgError::Other(msg),
302            crate::auth::AuthError::UnexpectedMessage => {
303                PgError::Auth("unexpected message during authentication".into())
304            }
305            crate::auth::AuthError::Protocol(p) => PgError::Protocol(p),
306            crate::auth::AuthError::Transport(t) => PgError::Transport(t),
307            crate::auth::AuthError::Io(i) => PgError::Io(i),
308            crate::auth::AuthError::Utf8(u) => PgError::Other(u.to_string()),
309        }
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Tests
315// ---------------------------------------------------------------------------
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use std::error::Error;
321
322    #[test]
323    fn test_pg_error_display() {
324        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
325            (b'S', "ERROR".to_string()),
326            (b'C', "23505".to_string()),
327            (b'M', "duplicate key".to_string()),
328        ])));
329        let display = err.to_string();
330        assert!(display.contains("server error"));
331        assert!(display.contains("duplicate key"));
332        assert!(display.contains("23505"));
333    }
334
335    #[test]
336    fn test_pg_error_is_connection_broken() {
337        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
338            (b'S', "FATAL".to_string()),
339            (b'C', "08006".to_string()),
340            (b'M', "connection failure".to_string()),
341        ])));
342        assert!(err.is_connection_broken());
343
344        assert!(PgError::Transport(TransportError::ConnectionReset).is_connection_broken());
345        assert!(PgError::Transport(TransportError::UnexpectedEof).is_connection_broken());
346        assert!(PgError::Transport(TransportError::ConnectionRefused).is_connection_broken());
347        assert!(!PgError::Transport(TransportError::Timeout).is_connection_broken());
348        assert!(PgError::ConnectionClosed.is_connection_broken());
349
350        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
351            (b'S', "FATAL".to_string()),
352            (b'C', "57P01".to_string()),
353            (b'M', "admin shutdown".to_string()),
354        ])));
355        assert!(err.is_connection_broken());
356
357        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
358            (b'S', "ERROR".to_string()),
359            (b'C', "23505".to_string()),
360            (b'M', "duplicate key".to_string()),
361        ])));
362        assert!(!err.is_connection_broken());
363    }
364
365    #[test]
366    fn test_pg_error_is_retryable() {
367        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
368            (b'S', "ERROR".to_string()),
369            (b'C', "40001".to_string()),
370            (b'M', "serialization failure".to_string()),
371        ])));
372        assert!(err.is_retryable());
373
374        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
375            (b'S', "ERROR".to_string()),
376            (b'C', "40P01".to_string()),
377            (b'M', "deadlock detected".to_string()),
378        ])));
379        assert!(err.is_retryable());
380
381        assert!(PgError::Transport(TransportError::Timeout).is_retryable());
382        assert!(PgError::Timeout.is_retryable());
383
384        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
385            (b'S', "ERROR".to_string()),
386            (b'C', "23505".to_string()),
387            (b'M', "duplicate key".to_string()),
388        ])));
389        assert!(!err.is_retryable());
390        assert!(!PgError::ConnectionClosed.is_retryable());
391    }
392
393    #[test]
394    fn test_pg_error_code() {
395        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
396            (b'S', "ERROR".to_string()),
397            (b'C', "23505".to_string()),
398            (b'M', "duplicate key".to_string()),
399        ])));
400        assert_eq!(err.code(), Some("23505"));
401        assert_eq!(PgError::ConnectionClosed.code(), None);
402    }
403
404    #[test]
405    fn test_pg_error_context() {
406        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
407            (b'S', "ERROR".to_string()),
408            (b'C', "23505".to_string()),
409            (b'M', "duplicate key".to_string()),
410        ])));
411        let with_ctx = err.context("inserting user");
412        match with_ctx {
413            PgError::Other(msg) => {
414                assert!(msg.contains("inserting user"));
415                assert!(msg.contains("duplicate key"));
416            }
417            _ => panic!("expected Other variant with context"),
418        }
419    }
420
421    #[test]
422    fn test_from_protocol_error() {
423        let err = PgError::from(ProtocolError::UnexpectedEof);
424        assert!(matches!(err, PgError::Protocol(_)));
425    }
426
427    #[test]
428    fn test_from_transport_error() {
429        let err = PgError::from(TransportError::ConnectionRefused);
430        assert!(matches!(err, PgError::Transport(_)));
431    }
432
433    #[test]
434    fn test_from_io_error() {
435        let err = PgError::from(std::io::Error::new(
436            std::io::ErrorKind::BrokenPipe,
437            "broken",
438        ));
439        assert!(matches!(err, PgError::Io(_)));
440    }
441
442    #[test]
443    fn test_io_error_is_connection_broken() {
444        let err = PgError::Io(std::io::Error::new(
445            std::io::ErrorKind::BrokenPipe,
446            "broken",
447        ));
448        assert!(err.is_connection_broken());
449
450        let err = PgError::Io(std::io::Error::new(
451            std::io::ErrorKind::ConnectionReset,
452            "reset",
453        ));
454        assert!(err.is_connection_broken());
455
456        let err = PgError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout"));
457        assert!(!err.is_connection_broken());
458    }
459
460    #[test]
461    fn test_error_source_chain() {
462        let err = PgError::Server(Box::new(PgServerError::from_fields(vec![
463            (b'S', "ERROR".to_string()),
464            (b'C', "23505".to_string()),
465            (b'M', "duplicate key".to_string()),
466        ])));
467        assert!(err.source().is_some());
468
469        let err = PgError::Transport(TransportError::ConnectionReset);
470        assert!(err.source().is_some());
471
472        let err = PgError::ConnectionClosed;
473        assert!(err.source().is_none());
474    }
475
476    #[test]
477    fn test_unexpected_null_display() {
478        let err = PgError::UnexpectedNull {
479            column: "id".to_string(),
480        };
481        assert_eq!(err.to_string(), "unexpected NULL in column id");
482    }
483
484    #[test]
485    fn test_column_not_found_display() {
486        let err = PgError::ColumnNotFound {
487            name: "email".to_string(),
488        };
489        assert_eq!(err.to_string(), "column not found: email");
490    }
491
492    #[test]
493    fn test_column_index_out_of_bounds_display() {
494        let err = PgError::ColumnIndexOutOfBounds { index: 5, count: 3 };
495        assert!(err.to_string().contains("5"));
496        assert!(err.to_string().contains("3"));
497    }
498}