#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("Database connection error: {0}")]
Connection(String),
#[error("Query blocked: only SELECT, SHOW, DESC, DESCRIBE, USE queries are allowed in read-only mode")]
ReadOnlyViolation,
#[error("Operation forbidden: LOAD_FILE() is not allowed for security reasons")]
LoadFileBlocked,
#[error("Operation forbidden: SELECT INTO OUTFILE/DUMPFILE is not allowed for security reasons")]
IntoOutfileBlocked,
#[error("Query blocked: only single statements are allowed")]
MultiStatement,
#[error("Invalid identifier '{0}': must not be empty, whitespace-only, or contain control characters")]
InvalidIdentifier(String),
#[error("Query timed out after {elapsed_secs:.1}s: {sql}")]
QueryTimeout {
elapsed_secs: f64,
sql: String,
},
#[error("Database error: {0}")]
Query(String),
#[error("Table not found: {0}")]
TableNotFound(String),
#[error("Serialization error: {0}")]
Serialization(String),
}
impl From<serde_json::Error> for AppError {
fn from(e: serde_json::Error) -> Self {
Self::Serialization(e.to_string())
}
}
impl From<AppError> for rmcp::model::ErrorData {
fn from(e: AppError) -> Self {
Self::internal_error(e.to_string(), None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_timeout_display_includes_elapsed_and_sql() {
let err = AppError::QueryTimeout {
elapsed_secs: 30.123_456,
sql: "SELECT * FROM big_table".into(),
};
let msg = err.to_string();
assert!(msg.contains("30.1"), "expected elapsed in message: {msg}");
assert!(
msg.contains("SELECT * FROM big_table"),
"expected SQL in message: {msg}"
);
}
}