1use std::io;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum Error {
9 #[error("connection error: {0}")]
11 Connection(String),
12
13 #[error("authentication failed: {0}")]
15 Authentication(String),
16
17 #[error("protocol error: {0}")]
19 Protocol(String),
20
21 #[error("sql error: {0}")]
23 Sql(String),
24
25 #[error("json decode error: {0}")]
27 JsonDecode(#[from] serde_json::Error),
28
29 #[error("io error: {0}")]
31 Io(#[from] io::Error),
32
33 #[error("invalid configuration: {0}")]
35 Config(String),
36
37 #[error("query cancelled")]
39 Cancelled,
40
41 #[error("invalid result schema: {0}")]
43 InvalidSchema(String),
44
45 #[error("connection busy: {0}")]
47 ConnectionBusy(String),
48
49 #[error("invalid connection state: expected {expected}, got {actual}")]
51 InvalidState {
52 expected: String,
54 actual: String,
56 },
57
58 #[error("connection closed")]
60 ConnectionClosed,
61
62 #[error("deserialization error for type '{type_name}': {details}")]
67 Deserialization {
68 type_name: String,
70 details: String,
72 },
73
74 #[error("memory limit exceeded: {estimated_memory} bytes buffered > {limit} bytes limit")]
89 MemoryLimitExceeded {
90 limit: usize,
92 estimated_memory: usize,
94 },
95}
96
97pub type Result<T> = std::result::Result<T, Error>;
99
100impl Error {
101 pub fn connection<S: Into<String>>(msg: S) -> Self {
103 Error::Connection(msg.into())
104 }
105
106 pub fn connection_refused(host: &str, port: u16) -> Self {
108 Error::Connection(format!(
109 "failed to connect to {}:{}: connection refused. \
110 Is Postgres running? Verify with: pg_isready -h {} -p {}",
111 host, port, host, port
112 ))
113 }
114
115 pub fn protocol<S: Into<String>>(msg: S) -> Self {
117 Error::Protocol(msg.into())
118 }
119
120 pub fn sql<S: Into<String>>(msg: S) -> Self {
122 Error::Sql(msg.into())
123 }
124
125 pub fn invalid_schema_columns(num_columns: usize) -> Self {
127 Error::InvalidSchema(format!(
128 "query returned {} columns instead of 1. \
129 fraiseql-wire supports only: SELECT data FROM <view>. \
130 See troubleshooting.md#error-invalid-result-schema",
131 num_columns
132 ))
133 }
134
135 pub fn invalid_schema<S: Into<String>>(msg: S) -> Self {
137 Error::InvalidSchema(msg.into())
138 }
139
140 pub fn auth_failed(username: &str, reason: &str) -> Self {
142 Error::Authentication(format!(
143 "authentication failed for user '{}': {}. \
144 Verify credentials with: psql -U {} -W",
145 username, reason, username
146 ))
147 }
148
149 pub fn config_invalid<S: Into<String>>(msg: S) -> Self {
151 Error::Config(format!(
152 "invalid configuration: {}. \
153 Expected format: postgres://[user[:password]@][host[:port]]/[database]",
154 msg.into()
155 ))
156 }
157
158 pub const fn is_retriable(&self) -> bool {
169 matches!(self, Error::Io(_) | Error::ConnectionClosed)
170 }
171
172 pub const fn category(&self) -> &'static str {
176 match self {
177 Error::Connection(_) => "connection",
178 Error::Authentication(_) => "authentication",
179 Error::Protocol(_) => "protocol",
180 Error::Sql(_) => "sql",
181 Error::JsonDecode(_) => "json_decode",
182 Error::Io(_) => "io",
183 Error::Config(_) => "config",
184 Error::Cancelled => "cancelled",
185 Error::InvalidSchema(_) => "invalid_schema",
186 Error::ConnectionBusy(_) => "connection_busy",
187 Error::InvalidState { .. } => "invalid_state",
188 Error::ConnectionClosed => "connection_closed",
189 Error::Deserialization { .. } => "deserialization",
190 Error::MemoryLimitExceeded { .. } => "memory_limit_exceeded",
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_error_helpers() {
201 let conn_err = Error::connection("failed to connect");
202 assert!(matches!(conn_err, Error::Connection(_)));
203
204 let proto_err = Error::protocol("unexpected message");
205 assert!(matches!(proto_err, Error::Protocol(_)));
206
207 let sql_err = Error::sql("syntax error");
208 assert!(matches!(sql_err, Error::Sql(_)));
209
210 let schema_err = Error::invalid_schema("expected single column");
211 assert!(matches!(schema_err, Error::InvalidSchema(_)));
212 }
213
214 #[test]
215 fn test_error_connection_refused() {
216 let err = Error::connection_refused("localhost", 5432);
217 let msg = err.to_string();
218 assert!(msg.contains("connection refused"));
219 assert!(msg.contains("Is Postgres running?"));
220 assert!(msg.contains("localhost"));
221 assert!(msg.contains("5432"));
222 }
223
224 #[test]
225 fn test_error_invalid_schema_columns() {
226 let err = Error::invalid_schema_columns(2);
227 let msg = err.to_string();
228 assert!(msg.contains("2 columns"));
229 assert!(msg.contains("instead of 1"));
230 assert!(msg.contains("SELECT data FROM"));
231 }
232
233 #[test]
234 fn test_error_auth_failed() {
235 let err = Error::auth_failed("postgres", "invalid password");
236 let msg = err.to_string();
237 assert!(msg.contains("postgres"));
238 assert!(msg.contains("invalid password"));
239 assert!(msg.contains("psql"));
240 }
241
242 #[test]
243 fn test_error_config_invalid() {
244 let err = Error::config_invalid("missing database name");
245 let msg = err.to_string();
246 assert!(msg.contains("invalid configuration"));
247 assert!(msg.contains("postgres://"));
248 assert!(msg.contains("missing database name"));
249 }
250
251 #[test]
252 fn test_error_category() {
253 assert_eq!(Error::connection("test").category(), "connection");
254 assert_eq!(Error::sql("test").category(), "sql");
255 assert_eq!(Error::Cancelled.category(), "cancelled");
256 assert_eq!(Error::ConnectionClosed.category(), "connection_closed");
257 }
258
259 #[test]
260 fn test_error_message_clarity() {
261 let err = Error::connection_refused("example.com", 5432);
263 let msg = err.to_string();
264
265 assert!(msg.contains("pg_isready"));
267
268 assert!(msg.contains("example.com"));
270 }
271
272 #[test]
273 fn test_is_retriable() {
274 assert!(Error::ConnectionClosed.is_retriable());
275 assert!(Error::Io(io::Error::new(io::ErrorKind::TimedOut, "timeout")).is_retriable());
276
277 assert!(!Error::connection("test").is_retriable());
278 assert!(!Error::sql("test").is_retriable());
279 assert!(!Error::invalid_schema("test").is_retriable());
280 }
281
282 #[test]
283 fn test_retriable_classification() {
284 assert!(Error::ConnectionClosed.is_retriable());
286 assert!(Error::Io(io::Error::new(io::ErrorKind::ConnectionReset, "reset")).is_retriable());
287
288 assert!(!Error::auth_failed("user", "invalid password").is_retriable());
290 assert!(!Error::sql("syntax error").is_retriable());
291 assert!(!Error::invalid_schema_columns(3).is_retriable());
292 }
293
294 #[test]
295 fn test_deserialization_error() {
296 let err = Error::Deserialization {
297 type_name: "Project".to_string(),
298 details: "missing field `id`".to_string(),
299 };
300 let msg = err.to_string();
301 assert!(msg.contains("Project"));
302 assert!(msg.contains("missing field"));
303 assert_eq!(err.category(), "deserialization");
304 }
305
306 #[test]
307 fn test_deserialization_error_not_retriable() {
308 let err = Error::Deserialization {
309 type_name: "User".to_string(),
310 details: "invalid type".to_string(),
311 };
312 assert!(!err.is_retriable());
313 }
314
315 #[test]
316 fn test_memory_limit_exceeded_error() {
317 let err = Error::MemoryLimitExceeded {
318 limit: 1_000_000,
319 estimated_memory: 1_500_000,
320 };
321 let msg = err.to_string();
322 assert!(msg.contains("1500000"));
323 assert!(msg.contains("1000000"));
324 assert!(msg.contains("memory limit exceeded"));
325 assert_eq!(err.category(), "memory_limit_exceeded");
326 }
327
328 #[test]
329 fn test_memory_limit_exceeded_not_retriable() {
330 let err = Error::MemoryLimitExceeded {
331 limit: 100_000,
332 estimated_memory: 150_000,
333 };
334 assert!(!err.is_retriable());
335 }
336}