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("pool cache is full ({cap} pools, all in use); retry later")]
36 PoolCacheFull {
37 cap: usize,
39 },
40
41 #[error("Query timed out after {elapsed_secs:.1}s: {sql}")]
43 QueryTimeout {
44 elapsed_secs: f64,
46 sql: String,
48 },
49
50 #[error("Database error: {0}")]
52 Query(String),
53
54 #[error("Table not found: {0}")]
56 TableNotFound(String),
57
58 #[error("Serialization error: {0}")]
60 Serialization(String),
61}
62
63impl From<serde_json::Error> for AppError {
64 fn from(e: serde_json::Error) -> Self {
65 Self::Serialization(e.to_string())
66 }
67}
68
69impl From<AppError> for rmcp::model::ErrorData {
70 fn from(e: AppError) -> Self {
71 Self::internal_error(e.to_string(), None)
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn query_timeout_display_includes_elapsed_and_sql() {
81 let err = AppError::QueryTimeout {
82 elapsed_secs: 30.123_456,
83 sql: "SELECT * FROM big_table".into(),
84 };
85 let msg = err.to_string();
86 assert!(msg.contains("30.1"), "expected elapsed in message: {msg}");
87 assert!(
88 msg.contains("SELECT * FROM big_table"),
89 "expected SQL in message: {msg}"
90 );
91 }
92}