1use rmcp::model::ErrorData;
4
5const CONNECTION_KIND: &str = "connection";
7
8#[derive(Debug, thiserror::Error)]
10pub enum SqlError {
11 #[error("Query blocked: only SELECT, SHOW, DESC, DESCRIBE, USE queries are allowed in read-only mode")]
13 ReadOnlyViolation,
14
15 #[error("Operation forbidden: LOAD_FILE() is not allowed for security reasons")]
17 LoadFileBlocked,
18
19 #[error("Operation forbidden: {0}() is not allowed for security reasons")]
21 DangerousFunctionBlocked(String),
22
23 #[error("Operation forbidden: SELECT INTO OUTFILE/DUMPFILE is not allowed for security reasons")]
25 IntoOutfileBlocked,
26
27 #[error("Query blocked: only single statements are allowed")]
29 MultiStatement,
30
31 #[error("Invalid identifier '{0}': must not be empty, whitespace-only, or contain control characters")]
33 InvalidIdentifier(String),
34
35 #[error("Query timed out after {elapsed_secs:.1}s: {sql}")]
37 QueryTimeout {
38 elapsed_secs: f64,
40 sql: String,
42 },
43
44 #[error("Database error: {0}")]
46 Query(String),
47
48 #[error("Database error: {0}")]
52 Connection(String),
53
54 #[error("Table not found: {0}")]
56 TableNotFound(String),
57}
58
59impl From<SqlError> for ErrorData {
60 fn from(e: SqlError) -> Self {
62 let data = matches!(e, SqlError::Connection(_)).then(|| serde_json::json!({ "kind": CONNECTION_KIND }));
63 Self::internal_error(e.to_string(), data)
64 }
65}
66
67#[must_use]
69pub fn is_connection_error(error: &ErrorData) -> bool {
70 error
71 .data
72 .as_ref()
73 .and_then(|data| data.get("kind"))
74 .is_some_and(|kind| kind == CONNECTION_KIND)
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn only_connection_errors_are_marked() {
83 let connection = ErrorData::from(SqlError::Connection("reset".into()));
84 assert_eq!(connection.message, "Database error: reset");
85 assert!(is_connection_error(&connection));
86
87 for error in [SqlError::Query("syntax".into()), SqlError::ReadOnlyViolation] {
88 assert!(!is_connection_error(&ErrorData::from(error)));
89 }
90 assert!(!is_connection_error(&ErrorData::internal_error("other", None)));
91 }
92}