use std::io;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, MigrationError>;
#[derive(Debug, Error)]
pub enum MigrationError {
#[error("invalid configuration: {0}")]
Config(String),
#[error("invalid connection string: {0}")]
InvalidConnectionString(String),
#[error("external command `{command}` failed: {message}")]
ExternalCommand {
command: String,
message: String,
},
#[error(
"required tool `{tool}` is not installed or not on $PATH: {reason}\n\
hint: install the matching PostgreSQL client tools (e.g. on Ubuntu \
`apt install postgresql-client-NN` where NN matches your source \
server's major version) and ensure they are on $PATH"
)]
MissingTool {
tool: String,
reason: String,
},
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("{}", format_pg_error(.0))]
Postgres(#[from] tokio_postgres::Error),
#[error("replication error: {0}")]
Replication(#[from] pg_walstream::ReplicationError),
#[error("apply error: {0}")]
Apply(String),
#[error("operation cancelled")]
Cancelled,
}
fn format_pg_error(err: &tokio_postgres::Error) -> String {
if let Some(db) = err.as_db_error() {
let mut msg = format!("postgres error: {}: {}", db.severity(), db.message());
if let Some(detail) = db.detail() {
msg.push_str("\nDETAIL: ");
msg.push_str(detail);
}
if let Some(hint) = db.hint() {
msg.push_str("\nHINT: ");
msg.push_str(hint);
}
msg
} else {
format!("postgres error: {err}")
}
}
impl MigrationError {
pub fn config(msg: impl Into<String>) -> Self {
Self::Config(msg.into())
}
pub fn external(command: impl Into<String>, message: impl Into<String>) -> Self {
Self::ExternalCommand {
command: command.into(),
message: message.into(),
}
}
pub fn apply(msg: impl Into<String>) -> Self {
Self::Apply(msg.into())
}
pub fn missing_tool(tool: impl Into<String>, reason: impl Into<String>) -> Self {
Self::MissingTool {
tool: tool.into(),
reason: reason.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_helper_constructs_variant() {
let err = MigrationError::config("bad");
assert!(matches!(err, MigrationError::Config(ref m) if m == "bad"));
assert_eq!(err.to_string(), "invalid configuration: bad");
}
#[test]
fn external_helper_formats_message() {
let err = MigrationError::external("pg_dump", "exit 1");
assert_eq!(err.to_string(), "external command `pg_dump` failed: exit 1");
}
#[test]
fn apply_helper_constructs_variant() {
let err = MigrationError::apply("oops");
assert!(matches!(err, MigrationError::Apply(ref m) if m == "oops"));
}
#[test]
fn missing_tool_helper_constructs_variant() {
let err = MigrationError::missing_tool("pg_dump", "not found in $PATH");
match err {
MigrationError::MissingTool {
ref tool,
ref reason,
} => {
assert_eq!(tool, "pg_dump");
assert_eq!(reason, "not found in $PATH");
}
_ => panic!("expected MissingTool variant"),
}
let msg = err.to_string();
assert!(msg.contains("pg_dump"));
assert!(msg.contains("not installed or not on $PATH"));
assert!(msg.contains("postgresql-client"));
}
#[test]
fn cancelled_display() {
let err = MigrationError::Cancelled;
assert_eq!(err.to_string(), "operation cancelled");
}
#[test]
fn from_io_error() {
let io_err = io::Error::new(io::ErrorKind::BrokenPipe, "pipe broke");
let err: MigrationError = io_err.into();
assert!(matches!(err, MigrationError::Io(_)));
assert!(err.to_string().contains("pipe broke"));
}
#[test]
fn format_pg_error_non_db_error() {
let err = "host not found".parse::<std::net::IpAddr>().unwrap_err();
let pg_err = tokio_postgres::Error::__private_api_timeout();
let formatted = format_pg_error(&pg_err);
assert!(formatted.starts_with("postgres error:"));
let _ = err;
}
#[test]
fn invalid_connection_string_display() {
let err = MigrationError::InvalidConnectionString("bad://url".into());
assert_eq!(err.to_string(), "invalid connection string: bad://url");
}
#[test]
fn replication_error_display() {
let rep_err = pg_walstream::ReplicationError::Protocol("test message".into());
let err: MigrationError = rep_err.into();
let msg = err.to_string();
assert!(msg.contains("replication error"));
}
#[test]
fn external_command_fields_accessible_via_match() {
let err = MigrationError::external("pg_restore", "exit code 2");
match &err {
MigrationError::ExternalCommand { command, message } => {
assert_eq!(command, "pg_restore");
assert_eq!(message, "exit code 2");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn io_error_preserves_kind() {
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "forbidden");
let err: MigrationError = io_err.into();
match err {
MigrationError::Io(ref e) => assert_eq!(e.kind(), io::ErrorKind::PermissionDenied),
_ => panic!("expected Io variant"),
}
assert!(err.to_string().contains("forbidden"));
}
#[test]
fn config_error_debug_format() {
let err = MigrationError::config("test cfg error");
let dbg = format!("{:?}", err);
assert!(dbg.contains("Config"));
assert!(dbg.contains("test cfg error"));
}
}