zero_postgres/
error.rs

1//! Error types for zero-postgres.
2
3use std::collections::HashMap;
4use thiserror::Error;
5
6/// Result type for zero-postgres operations.
7pub type Result<T> = core::result::Result<T, Error>;
8
9/// PostgreSQL error field type codes.
10pub mod field_type {
11    pub const SEVERITY: u8 = b'S';
12    pub const SEVERITY_V: u8 = b'V';
13    pub const CODE: u8 = b'C';
14    pub const MESSAGE: u8 = b'M';
15    pub const DETAIL: u8 = b'D';
16    pub const HINT: u8 = b'H';
17    pub const POSITION: u8 = b'P';
18    pub const INTERNAL_POSITION: u8 = b'p';
19    pub const INTERNAL_QUERY: u8 = b'q';
20    pub const WHERE: u8 = b'W';
21    pub const SCHEMA: u8 = b's';
22    pub const TABLE: u8 = b't';
23    pub const COLUMN: u8 = b'c';
24    pub const DATA_TYPE: u8 = b'd';
25    pub const CONSTRAINT: u8 = b'n';
26    pub const FILE: u8 = b'F';
27    pub const LINE: u8 = b'L';
28    pub const ROUTINE: u8 = b'R';
29}
30
31/// PostgreSQL server error/notice message.
32#[derive(Debug, Clone)]
33pub struct ServerError(pub(crate) HashMap<u8, String>);
34
35impl ServerError {
36    /// Create from a HashMap of field codes to values.
37    pub fn new(fields: HashMap<u8, String>) -> Self {
38        Self(fields)
39    }
40
41    // Always present (PostgreSQL 9.6+)
42
43    /// Severity (localized): ERROR, FATAL, PANIC, WARNING, NOTICE, DEBUG, INFO, LOG
44    pub fn severity(&self) -> &str {
45        self.0
46            .get(&field_type::SEVERITY)
47            .map(|s| s.as_str())
48            .unwrap_or_default()
49    }
50
51    /// Severity (non-localized, never translated)
52    pub fn severity_v(&self) -> &str {
53        self.0
54            .get(&field_type::SEVERITY_V)
55            .map(|s| s.as_str())
56            .unwrap_or_default()
57    }
58
59    /// SQLSTATE error code (5 characters)
60    pub fn code(&self) -> &str {
61        self.0
62            .get(&field_type::CODE)
63            .map(|s| s.as_str())
64            .unwrap_or_default()
65    }
66
67    /// Primary error message
68    pub fn message(&self) -> &str {
69        self.0
70            .get(&field_type::MESSAGE)
71            .map(|s| s.as_str())
72            .unwrap_or_default()
73    }
74
75    // Optional fields
76
77    /// Detailed error explanation
78    pub fn detail(&self) -> Option<&str> {
79        self.0.get(&field_type::DETAIL).map(|s| s.as_str())
80    }
81
82    /// Suggestion for fixing the error
83    pub fn hint(&self) -> Option<&str> {
84        self.0.get(&field_type::HINT).map(|s| s.as_str())
85    }
86
87    /// Cursor position in query string (1-based)
88    pub fn position(&self) -> Option<u32> {
89        self.0.get(&field_type::POSITION)?.parse().ok()
90    }
91
92    /// Position in internal query
93    pub fn internal_position(&self) -> Option<u32> {
94        self.0.get(&field_type::INTERNAL_POSITION)?.parse().ok()
95    }
96
97    /// Failed internal command text
98    pub fn internal_query(&self) -> Option<&str> {
99        self.0.get(&field_type::INTERNAL_QUERY).map(|s| s.as_str())
100    }
101
102    /// Context/stack trace
103    pub fn where_(&self) -> Option<&str> {
104        self.0.get(&field_type::WHERE).map(|s| s.as_str())
105    }
106
107    /// Schema name
108    pub fn schema(&self) -> Option<&str> {
109        self.0.get(&field_type::SCHEMA).map(|s| s.as_str())
110    }
111
112    /// Table name
113    pub fn table(&self) -> Option<&str> {
114        self.0.get(&field_type::TABLE).map(|s| s.as_str())
115    }
116
117    /// Column name
118    pub fn column(&self) -> Option<&str> {
119        self.0.get(&field_type::COLUMN).map(|s| s.as_str())
120    }
121
122    /// Data type name
123    pub fn data_type(&self) -> Option<&str> {
124        self.0.get(&field_type::DATA_TYPE).map(|s| s.as_str())
125    }
126
127    /// Constraint name
128    pub fn constraint(&self) -> Option<&str> {
129        self.0.get(&field_type::CONSTRAINT).map(|s| s.as_str())
130    }
131
132    /// Source file name
133    pub fn file(&self) -> Option<&str> {
134        self.0.get(&field_type::FILE).map(|s| s.as_str())
135    }
136
137    /// Source line number
138    pub fn line(&self) -> Option<u32> {
139        self.0.get(&field_type::LINE)?.parse().ok()
140    }
141
142    /// Source routine name
143    pub fn routine(&self) -> Option<&str> {
144        self.0.get(&field_type::ROUTINE).map(|s| s.as_str())
145    }
146
147    /// Get a field by its type code.
148    pub fn get(&self, field_type: u8) -> Option<&str> {
149        self.0.get(&field_type).map(|s| s.as_str())
150    }
151}
152
153impl std::fmt::Display for ServerError {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        write!(
156            f,
157            "{}: {} (SQLSTATE {})",
158            self.severity(),
159            self.message(),
160            self.code()
161        )?;
162        if let Some(detail) = self.detail() {
163            write!(f, "\nDETAIL: {}", detail)?;
164        }
165        if let Some(hint) = self.hint() {
166            write!(f, "\nHINT: {}", hint)?;
167        }
168        Ok(())
169    }
170}
171
172/// Error type for zero-postgres.
173#[derive(Debug, Error)]
174pub enum Error {
175    /// Server error response
176    #[error("PostgreSQL error: {0}")]
177    Server(ServerError),
178
179    /// Protocol error (malformed message, unexpected response, etc.)
180    #[error("Protocol error: {0}")]
181    Protocol(String),
182
183    /// I/O error
184    #[error("I/O error: {0}")]
185    Io(#[from] std::io::Error),
186
187    /// Authentication failed
188    #[error("Authentication failed: {0}")]
189    Auth(String),
190
191    /// TLS error
192    #[cfg(any(feature = "sync-tls", feature = "tokio-tls"))]
193    #[error("TLS error: {0}")]
194    Tls(#[from] native_tls::Error),
195
196    /// Connection is broken and cannot be reused
197    #[error("Connection is broken")]
198    ConnectionBroken,
199
200    /// Invalid usage (e.g., nested transactions)
201    #[error("Invalid usage: {0}")]
202    InvalidUsage(String),
203
204    /// Unsupported feature
205    #[error("Unsupported: {0}")]
206    Unsupported(String),
207
208    /// Value decode error
209    #[error("Decode error: {0}")]
210    Decode(String),
211
212    /// Value encode error
213    #[error("Encode error: {0}")]
214    Encode(String),
215}
216
217impl Error {
218    /// Create an overflow error when a value cannot be converted to a target type.
219    pub fn overflow(from: &str, to: &str) -> Self {
220        Error::Encode(format!("value overflow: cannot convert {} to {}", from, to))
221    }
222
223    /// Create a type mismatch error when encoding to an incompatible OID.
224    pub fn type_mismatch(value_oid: u32, target_oid: u32) -> Self {
225        Error::Encode(format!(
226            "type mismatch: value has OID {} but target expects OID {}",
227            value_oid, target_oid
228        ))
229    }
230}
231
232impl Error {
233    /// Returns true if the error indicates the connection is broken and cannot be reused.
234    pub fn is_connection_broken(&self) -> bool {
235        match self {
236            Error::Io(_) | Error::ConnectionBroken => true,
237            Error::Server(err) => {
238                // FATAL and PANIC errors indicate connection is broken
239                matches!(err.severity_v(), "FATAL" | "PANIC")
240            }
241            _ => false,
242        }
243    }
244
245    /// Get the SQLSTATE code if this is a server error.
246    pub fn sqlstate(&self) -> Option<&str> {
247        match self {
248            Error::Server(err) => Some(err.code()),
249            _ => None,
250        }
251    }
252}
253
254impl<Src: std::fmt::Debug, Dst: std::fmt::Debug + ?Sized> From<zerocopy::error::CastError<Src, Dst>>
255    for Error
256{
257    fn from(err: zerocopy::error::CastError<Src, Dst>) -> Self {
258        Error::Protocol(format!("zerocopy cast error: {err:?}"))
259    }
260}
261
262impl From<std::convert::Infallible> for Error {
263    fn from(err: std::convert::Infallible) -> Self {
264        match err {}
265    }
266}