database_mcp_server/
error.rs1#[derive(Debug, thiserror::Error)]
9pub enum AppError {
10 #[error("Database connection error: {0}")]
12 Connection(String),
13
14 #[error("Query blocked: only SELECT, SHOW, DESC, DESCRIBE, USE queries are allowed in read-only mode")]
16 ReadOnlyViolation,
17
18 #[error("Operation forbidden: LOAD_FILE() is not allowed for security reasons")]
20 LoadFileBlocked,
21
22 #[error("Operation forbidden: SELECT INTO OUTFILE/DUMPFILE is not allowed for security reasons")]
24 IntoOutfileBlocked,
25
26 #[error("Query blocked: only single statements are allowed")]
28 MultiStatement,
29
30 #[error("Invalid identifier '{0}': must not be empty, whitespace-only, or contain control characters")]
32 InvalidIdentifier(String),
33
34 #[error("Query timed out after {elapsed_secs:.1}s: {sql}")]
36 QueryTimeout {
37 elapsed_secs: f64,
39 sql: String,
41 },
42
43 #[error("Database error: {0}")]
45 Query(String),
46
47 #[error("Table not found: {0}")]
49 TableNotFound(String),
50
51 #[error("Serialization error: {0}")]
53 Serialization(String),
54}
55
56impl From<serde_json::Error> for AppError {
57 fn from(e: serde_json::Error) -> Self {
58 Self::Serialization(e.to_string())
59 }
60}
61
62impl From<AppError> for rmcp::model::ErrorData {
63 fn from(e: AppError) -> Self {
64 Self::internal_error(e.to_string(), None)
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn query_timeout_display_includes_elapsed_and_sql() {
74 let err = AppError::QueryTimeout {
75 elapsed_secs: 30.123_456,
76 sql: "SELECT * FROM big_table".into(),
77 };
78 let msg = err.to_string();
79 assert!(msg.contains("30.1"), "expected elapsed in message: {msg}");
80 assert!(
81 msg.contains("SELECT * FROM big_table"),
82 "expected SQL in message: {msg}"
83 );
84 }
85}