Skip to main content

zero_postgres/
error.rs

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