Skip to main content

fraiseql_wire/error/
mod.rs

1//! Error types for fraiseql-wire
2
3use std::io;
4use thiserror::Error;
5
6/// Main error type for fraiseql-wire operations
7#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum WireError {
10    /// Connection error
11    #[error("connection error: {0}")]
12    Connection(String),
13
14    /// Authentication error
15    #[error("authentication failed: {0}")]
16    Authentication(String),
17
18    /// Protocol violation
19    #[error("protocol error: {0}")]
20    Protocol(String),
21
22    /// SQL execution error
23    #[error("sql error: {0}")]
24    Sql(String),
25
26    /// JSON decoding error
27    #[error("json decode error: {0}")]
28    JsonDecode(#[from] serde_json::Error),
29
30    /// I/O error
31    #[error("io error: {0}")]
32    Io(#[from] io::Error),
33
34    /// Invalid configuration
35    #[error("invalid configuration: {0}")]
36    Config(String),
37
38    /// Query cancelled by client
39    #[error("query cancelled")]
40    Cancelled,
41
42    /// Invalid result schema (not single `data` column)
43    #[error("invalid result schema: {0}")]
44    InvalidSchema(String),
45
46    /// Connection already in use
47    #[error("connection busy: {0}")]
48    ConnectionBusy(String),
49
50    /// Invalid connection state
51    #[error("invalid connection state: expected {expected}, got {actual}")]
52    InvalidState {
53        /// Expected state
54        expected: String,
55        /// Actual state
56        actual: String,
57    },
58
59    /// Connection closed
60    #[error("connection closed")]
61    ConnectionClosed,
62
63    /// Type deserialization error
64    ///
65    /// Occurs when a row cannot be deserialized into the target type.
66    /// This is a consumer-side error that includes the type name and serde details.
67    #[error("deserialization error for type '{type_name}': {details}")]
68    Deserialization {
69        /// Name of the type we were deserializing to
70        type_name: String,
71        /// Details from `serde_json` about what went wrong
72        details: String,
73    },
74
75    /// Memory limit exceeded
76    ///
77    /// **Terminal error**: The consumer cannot keep pace with data arrival.
78    ///
79    /// Occurs when estimated buffered memory exceeds the configured maximum.
80    /// This indicates the consumer is too slow relative to data arrival rate.
81    ///
82    /// NOT retriable: Retrying the same query with the same consumer will hit the same limit.
83    ///
84    /// Solutions:
85    /// 1. Increase consumer throughput (faster `.next()` polling)
86    /// 2. Reduce items in flight (configure lower `chunk_size`)
87    /// 3. Remove memory limit (use unbounded mode)
88    /// 4. Use different transport (consider `tokio-postgres` for flexibility)
89    #[error("memory limit exceeded: {estimated_memory} bytes buffered > {limit} bytes limit")]
90    MemoryLimitExceeded {
91        /// Configured memory limit in bytes
92        limit: usize,
93        /// Current estimated memory in bytes (`items_buffered` * 2048)
94        estimated_memory: usize,
95    },
96}
97
98/// Result type alias using fraiseql-wire [`WireError`]
99pub type Result<T> = std::result::Result<T, WireError>;
100
101impl WireError {
102    /// Create a connection error with context
103    pub fn connection<S: Into<String>>(msg: S) -> Self {
104        WireError::Connection(msg.into())
105    }
106
107    /// Create a connection refused error (helpful message for debugging)
108    #[must_use]
109    pub fn connection_refused(host: &str, port: u16) -> Self {
110        WireError::Connection(format!(
111            "failed to connect to {}:{}: connection refused. \
112            Is Postgres running? Verify with: pg_isready -h {} -p {}",
113            host, port, host, port
114        ))
115    }
116
117    /// Create a protocol error with context
118    pub fn protocol<S: Into<String>>(msg: S) -> Self {
119        WireError::Protocol(msg.into())
120    }
121
122    /// Create a SQL error with context
123    pub fn sql<S: Into<String>>(msg: S) -> Self {
124        WireError::Sql(msg.into())
125    }
126
127    /// Create a schema validation error (query returned wrong columns)
128    #[must_use]
129    pub fn invalid_schema_columns(num_columns: usize) -> Self {
130        WireError::InvalidSchema(format!(
131            "query returned {} columns instead of 1. \
132            fraiseql-wire supports only: SELECT data FROM <view>. \
133            See troubleshooting.md#error-invalid-result-schema",
134            num_columns
135        ))
136    }
137
138    /// Create an invalid schema error with context
139    pub fn invalid_schema<S: Into<String>>(msg: S) -> Self {
140        WireError::InvalidSchema(msg.into())
141    }
142
143    /// Create an authentication error with helpful message
144    #[must_use]
145    pub fn auth_failed(username: &str, reason: &str) -> Self {
146        WireError::Authentication(format!(
147            "authentication failed for user '{}': {}. \
148            Verify credentials with: psql -U {} -W",
149            username, reason, username
150        ))
151    }
152
153    /// Create a config error with helpful message
154    pub fn config_invalid<S: Into<String>>(msg: S) -> Self {
155        WireError::Config(format!(
156            "invalid configuration: {}. \
157            Expected format: postgres://[user[:password]@][host[:port]]/[database]",
158            msg.into()
159        ))
160    }
161
162    /// Check if error is retriable (transient)
163    ///
164    /// Retriable errors typically indicate temporary issues that may succeed on retry:
165    /// - I/O errors (network timeouts, etc.)
166    /// - Connection closed (can reconnect)
167    ///
168    /// Non-retriable errors indicate permanent problems:
169    /// - Invalid schema (won't change between attempts)
170    /// - Invalid configuration (needs user intervention)
171    /// - SQL errors (query is invalid)
172    #[must_use]
173    pub const fn is_retriable(&self) -> bool {
174        matches!(self, WireError::Io(_) | WireError::ConnectionClosed)
175    }
176
177    /// Get error category for observability and logging
178    ///
179    /// Used to categorize errors for metrics, tracing, and error handling decisions.
180    #[must_use]
181    pub const fn category(&self) -> &'static str {
182        match self {
183            WireError::Connection(_) => "connection",
184            WireError::Authentication(_) => "authentication",
185            WireError::Protocol(_) => "protocol",
186            WireError::Sql(_) => "sql",
187            WireError::JsonDecode(_) => "json_decode",
188            WireError::Io(_) => "io",
189            WireError::Config(_) => "config",
190            WireError::Cancelled => "cancelled",
191            WireError::InvalidSchema(_) => "invalid_schema",
192            WireError::ConnectionBusy(_) => "connection_busy",
193            WireError::InvalidState { .. } => "invalid_state",
194            WireError::ConnectionClosed => "connection_closed",
195            WireError::Deserialization { .. } => "deserialization",
196            WireError::MemoryLimitExceeded { .. } => "memory_limit_exceeded",
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests;