use std::io;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum WireError {
#[error("connection error: {0}")]
Connection(String),
#[error("authentication failed: {0}")]
Authentication(String),
#[error("protocol error: {0}")]
Protocol(String),
#[error("sql error: {0}")]
Sql(String),
#[error("json decode error: {0}")]
JsonDecode(#[from] serde_json::Error),
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("invalid configuration: {0}")]
Config(String),
#[error("query cancelled")]
Cancelled,
#[error("invalid result schema: {0}")]
InvalidSchema(String),
#[error("connection busy: {0}")]
ConnectionBusy(String),
#[error("invalid connection state: expected {expected}, got {actual}")]
InvalidState {
expected: String,
actual: String,
},
#[error("connection closed")]
ConnectionClosed,
#[error("deserialization error for type '{type_name}': {details}")]
Deserialization {
type_name: String,
details: String,
},
#[error("memory limit exceeded: {estimated_memory} bytes buffered > {limit} bytes limit")]
MemoryLimitExceeded {
limit: usize,
estimated_memory: usize,
},
}
pub type Result<T> = std::result::Result<T, WireError>;
impl WireError {
pub fn connection<S: Into<String>>(msg: S) -> Self {
WireError::Connection(msg.into())
}
pub fn connection_refused(host: &str, port: u16) -> Self {
WireError::Connection(format!(
"failed to connect to {}:{}: connection refused. \
Is Postgres running? Verify with: pg_isready -h {} -p {}",
host, port, host, port
))
}
pub fn protocol<S: Into<String>>(msg: S) -> Self {
WireError::Protocol(msg.into())
}
pub fn sql<S: Into<String>>(msg: S) -> Self {
WireError::Sql(msg.into())
}
pub fn invalid_schema_columns(num_columns: usize) -> Self {
WireError::InvalidSchema(format!(
"query returned {} columns instead of 1. \
fraiseql-wire supports only: SELECT data FROM <view>. \
See troubleshooting.md#error-invalid-result-schema",
num_columns
))
}
pub fn invalid_schema<S: Into<String>>(msg: S) -> Self {
WireError::InvalidSchema(msg.into())
}
pub fn auth_failed(username: &str, reason: &str) -> Self {
WireError::Authentication(format!(
"authentication failed for user '{}': {}. \
Verify credentials with: psql -U {} -W",
username, reason, username
))
}
pub fn config_invalid<S: Into<String>>(msg: S) -> Self {
WireError::Config(format!(
"invalid configuration: {}. \
Expected format: postgres://[user[:password]@][host[:port]]/[database]",
msg.into()
))
}
pub const fn is_retriable(&self) -> bool {
matches!(self, WireError::Io(_) | WireError::ConnectionClosed)
}
pub const fn category(&self) -> &'static str {
match self {
WireError::Connection(_) => "connection",
WireError::Authentication(_) => "authentication",
WireError::Protocol(_) => "protocol",
WireError::Sql(_) => "sql",
WireError::JsonDecode(_) => "json_decode",
WireError::Io(_) => "io",
WireError::Config(_) => "config",
WireError::Cancelled => "cancelled",
WireError::InvalidSchema(_) => "invalid_schema",
WireError::ConnectionBusy(_) => "connection_busy",
WireError::InvalidState { .. } => "invalid_state",
WireError::ConnectionClosed => "connection_closed",
WireError::Deserialization { .. } => "deserialization",
WireError::MemoryLimitExceeded { .. } => "memory_limit_exceeded",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_helpers() {
let conn_err = WireError::connection("failed to connect");
assert!(matches!(conn_err, WireError::Connection(_)));
let proto_err = WireError::protocol("unexpected message");
assert!(matches!(proto_err, WireError::Protocol(_)));
let sql_err = WireError::sql("syntax error");
assert!(matches!(sql_err, WireError::Sql(_)));
let schema_err = WireError::invalid_schema("expected single column");
assert!(matches!(schema_err, WireError::InvalidSchema(_)));
}
#[test]
fn test_error_connection_refused() {
let err = WireError::connection_refused("localhost", 5432);
let msg = err.to_string();
assert!(msg.contains("connection refused"));
assert!(msg.contains("Is Postgres running?"));
assert!(msg.contains("localhost"));
assert!(msg.contains("5432"));
}
#[test]
fn test_error_invalid_schema_columns() {
let err = WireError::invalid_schema_columns(2);
let msg = err.to_string();
assert!(msg.contains("2 columns"));
assert!(msg.contains("instead of 1"));
assert!(msg.contains("SELECT data FROM"));
}
#[test]
fn test_error_auth_failed() {
let err = WireError::auth_failed("postgres", "invalid password");
let msg = err.to_string();
assert!(msg.contains("postgres"));
assert!(msg.contains("invalid password"));
assert!(msg.contains("psql"));
}
#[test]
fn test_error_config_invalid() {
let err = WireError::config_invalid("missing database name");
let msg = err.to_string();
assert!(msg.contains("invalid configuration"));
assert!(msg.contains("postgres://"));
assert!(msg.contains("missing database name"));
}
#[test]
fn test_error_category() {
assert_eq!(WireError::connection("test").category(), "connection");
assert_eq!(WireError::sql("test").category(), "sql");
assert_eq!(WireError::Cancelled.category(), "cancelled");
assert_eq!(WireError::ConnectionClosed.category(), "connection_closed");
}
#[test]
fn test_error_message_clarity() {
let err = WireError::connection_refused("example.com", 5432);
let msg = err.to_string();
assert!(msg.contains("pg_isready"));
assert!(msg.contains("example.com"));
}
#[test]
fn test_is_retriable() {
assert!(WireError::ConnectionClosed.is_retriable());
assert!(WireError::Io(io::Error::new(io::ErrorKind::TimedOut, "timeout")).is_retriable());
assert!(!WireError::connection("test").is_retriable());
assert!(!WireError::sql("test").is_retriable());
assert!(!WireError::invalid_schema("test").is_retriable());
}
#[test]
fn test_retriable_classification() {
assert!(WireError::ConnectionClosed.is_retriable());
assert!(
WireError::Io(io::Error::new(io::ErrorKind::ConnectionReset, "reset")).is_retriable()
);
assert!(!WireError::auth_failed("user", "invalid password").is_retriable());
assert!(!WireError::sql("syntax error").is_retriable());
assert!(!WireError::invalid_schema_columns(3).is_retriable());
}
#[test]
fn test_deserialization_error() {
let err = WireError::Deserialization {
type_name: "Project".to_string(),
details: "missing field `id`".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("Project"));
assert!(msg.contains("missing field"));
assert_eq!(err.category(), "deserialization");
}
#[test]
fn test_deserialization_error_not_retriable() {
let err = WireError::Deserialization {
type_name: "User".to_string(),
details: "invalid type".to_string(),
};
assert!(!err.is_retriable());
}
#[test]
fn test_memory_limit_exceeded_error() {
let err = WireError::MemoryLimitExceeded {
limit: 1_000_000,
estimated_memory: 1_500_000,
};
let msg = err.to_string();
assert!(msg.contains("1500000"));
assert!(msg.contains("1000000"));
assert!(msg.contains("memory limit exceeded"));
assert_eq!(err.category(), "memory_limit_exceeded");
}
#[test]
fn test_memory_limit_exceeded_not_retriable() {
let err = WireError::MemoryLimitExceeded {
limit: 100_000,
estimated_memory: 150_000,
};
assert!(!err.is_retriable());
}
}