use thiserror::Error;
#[allow(dead_code)]
#[derive(Error, Debug, Clone)]
pub enum DbError {
#[error("Connection failed: {0}")]
ConnectionFailed(String),
#[error("Query failed: {0}")]
QueryFailed(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Not supported: {0}")]
NotSupported(String),
#[error("Timeout")]
Timeout,
#[error("Unknown error: {0}")]
Unknown(String),
}
#[allow(dead_code)]
#[derive(Error, Debug, Clone)]
pub enum UiError {
#[error("Render failed: {0}")]
RenderFailed(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("State error: {0}")]
StateError(String),
}
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Db(#[from] DbError),
#[error("UI error: {0}")]
Ui(#[from] UiError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Storage error: {0}")]
Storage(String),
}
pub type DbResult<T> = std::result::Result<T, DbError>;
pub type AppResult<T> = std::result::Result<T, AppError>;
pub fn friendly_connect_error(db_type: crate::core::models::DatabaseType, raw: &str) -> String {
use crate::core::models::DatabaseType;
let lc = raw.to_lowercase();
if matches!(db_type, DatabaseType::Oracle)
&& let Some((title, hint)) = oracle_hint(&lc)
{
return format_error(title, raw, Some(hint));
}
if lc.contains("connection refused")
|| lc.contains("no route to host")
|| lc.contains("network is unreachable")
{
return format_error(
"Can't reach the server",
raw,
Some(
"Check the host/port and that the database is running and accepts remote connections.",
),
);
}
if lc.contains("password authentication failed")
|| lc.contains("access denied for user")
|| lc.contains("authentication failed")
{
return format_error(
"Invalid credentials",
raw,
Some("The server rejected the username or password."),
);
}
if lc.contains("timed out") || lc.contains("timeout") || lc.contains("timer expired") {
return format_error(
"Connection timeout",
raw,
Some("The server didn't answer in time — firewall, VPN, or wrong host?"),
);
}
if lc.contains("no such host")
|| lc.contains("failed to lookup address")
|| lc.contains("name or service not known")
|| lc.contains("nodename nor servname")
{
return format_error(
"Host not found",
raw,
Some("DNS couldn't resolve the host — check the host field for typos."),
);
}
if (lc.contains("database") && lc.contains("does not exist")) || lc.contains("unknown database")
{
return format_error(
"Database does not exist",
raw,
Some("Check the database/schema name in the connection form."),
);
}
if lc.contains("ssl") || lc.contains("tls") {
return format_error(
"SSL/TLS error",
raw,
Some("Server may require SSL, or the certificate could not be verified."),
);
}
if lc.contains("too many connections") {
return format_error(
"Too many connections",
raw,
Some("The server has reached its connection limit — retry shortly."),
);
}
if lc.contains("role") && lc.contains("does not exist") {
return format_error(
"Unknown role",
raw,
Some("Verify the username exists on the server."),
);
}
format_error("Connection failed", raw, None)
}
fn oracle_hint(lc: &str) -> Option<(&'static str, &'static str)> {
let table: &[(&str, &str, &str)] = &[
(
"ora-12541",
"Listener not running",
"The Oracle TNS listener isn't reachable — verify host/port and that the listener is up.",
),
(
"ora-12514",
"Unknown service name",
"Verify the Service Name / SID in the connection string — the listener doesn't know this service.",
),
(
"ora-12505",
"Unknown SID",
"The listener doesn't know this SID — check Database field or switch to Service Name.",
),
(
"ora-01017",
"Invalid username/password",
"Oracle rejected your credentials. Note: passwords may be case-sensitive.",
),
(
"ora-28000",
"Account locked",
"The Oracle account is locked — ask a DBA to run `ALTER USER <user> ACCOUNT UNLOCK`.",
),
(
"ora-28001",
"Password has expired",
"The password must be changed on the server before you can connect.",
),
(
"ora-12545",
"Host not found",
"DNS couldn't resolve the target host — verify it's correct.",
),
(
"ora-12170",
"TNS connect timeout",
"Firewall/VPN may be dropping packets to the listener port.",
),
(
"ora-12154",
"TNS could not resolve connect identifier",
"Check the service name / connect string — it didn't match any TNS alias.",
),
(
"ora-12537",
"TNS connection closed",
"The listener accepted the connection then dropped it — possible misconfiguration on the server.",
),
(
"ora-12560",
"TNS protocol adapter error",
"The Oracle client couldn't initialise — check ORACLE_HOME / instant client install.",
),
(
"ora-01034",
"Oracle not available",
"The database instance is down (not just the listener).",
),
(
"ora-01005",
"Null password given",
"Password field is empty.",
),
(
"ora-00942",
"Table or view does not exist",
"The credentials worked, but the schema lacks the expected objects.",
),
];
for (code, title, hint) in table {
if lc.contains(code) {
return Some((*title, *hint));
}
}
None
}
fn format_error(title: &str, detail: &str, hint: Option<&str>) -> String {
let detail = detail.trim();
let mut out = title.to_string();
if !detail.is_empty() && !detail.eq_ignore_ascii_case(title) {
out.push('\n');
out.push_str(detail);
}
if let Some(h) = hint {
out.push('\n');
out.push_str("Hint: ");
out.push_str(h);
}
out
}
#[cfg(test)]
mod friendly_tests {
use super::*;
use crate::core::models::DatabaseType;
#[test]
fn oracle_ora_01017_invalid_credentials() {
let msg = friendly_connect_error(
DatabaseType::Oracle,
"ORA-01017: invalid username/password; logon denied",
);
assert!(msg.starts_with("Invalid username/password"));
assert!(msg.contains("Hint:"));
}
#[test]
fn oracle_listener_down() {
let msg = friendly_connect_error(DatabaseType::Oracle, "ORA-12541: TNS:no listener");
assert!(msg.starts_with("Listener not running"));
}
#[test]
fn postgres_auth_failure() {
let msg = friendly_connect_error(
DatabaseType::PostgreSQL,
"error returned from database: password authentication failed for user \"bob\"",
);
assert!(msg.starts_with("Invalid credentials"));
}
#[test]
fn network_refused() {
let msg = friendly_connect_error(
DatabaseType::PostgreSQL,
"error communicating with the server: Connection refused (os error 111)",
);
assert!(msg.starts_with("Can't reach the server"));
}
#[test]
fn fallback_keeps_raw() {
let msg = friendly_connect_error(DatabaseType::MySQL, "something truly weird");
assert!(msg.starts_with("Connection failed"));
assert!(msg.contains("something truly weird"));
}
}