Skip to main content

krishiv_sql/
sqlstate.rs

1#![forbid(unsafe_code)]
2//! SQLSTATE code mapping for Krishiv SQL errors.
3//!
4//! Maps [`SqlError`] variants to the 5-character SQLSTATE codes defined by
5//! ISO/IEC 9075 (SQL standard) and widely adopted by JDBC/ODBC drivers.
6//! Clients can surface these codes over the Flight SQL wire protocol in the
7//! `grpc-status-details` trailer.
8
9use crate::SqlError;
10
11// ── Well-known SQLSTATE codes ─────────────────────────────────────────────────
12
13/// `00000` — Successful completion.
14pub const SUCCESS: &str = "00000";
15/// `0A000` — Feature not supported.
16pub const FEATURE_NOT_SUPPORTED: &str = "0A000";
17/// `22000` — Data exception (general).
18pub const DATA_EXCEPTION: &str = "22000";
19/// `28000` — Invalid authorisation specification (access denied).
20pub const INVALID_AUTHORIZATION: &str = "28000";
21/// `42000` — Syntax error or access rule violation.
22pub const SYNTAX_ERROR: &str = "42000";
23/// `42501` — Insufficient privilege.
24pub const INSUFFICIENT_PRIVILEGE: &str = "42501";
25/// `42P01` — Undefined table.
26pub const UNDEFINED_TABLE: &str = "42P01";
27/// `57014` — Query cancelled (due to operator or timeout).
28pub const QUERY_CANCELLED: &str = "57014";
29/// `57P05` — Query execution timeout.
30pub const QUERY_TIMEOUT: &str = "57P05";
31/// `58000` — System error (external component failure).
32pub const SYSTEM_ERROR: &str = "58000";
33/// `XX000` — Internal error (engine fault).
34pub const INTERNAL_ERROR: &str = "XX000";
35/// `HY000` — General error (catch-all for driver-level errors).
36pub const GENERAL_ERROR: &str = "HY000";
37
38// ── Mapping ───────────────────────────────────────────────────────────────────
39
40/// Return the SQLSTATE code for the given [`SqlError`].
41///
42/// The returned string is always a 5-character SQLSTATE code conforming to
43/// ISO/IEC 9075.
44pub fn sqlstate_for(error: &SqlError) -> &'static str {
45    match error {
46        SqlError::EmptyQuery => SYNTAX_ERROR,
47        SqlError::EmptyTableName => SYNTAX_ERROR,
48        SqlError::Unsupported { .. } => FEATURE_NOT_SUPPORTED,
49        SqlError::InvalidTableFunction { .. } => SYNTAX_ERROR,
50        SqlError::DataFusion { .. } => INTERNAL_ERROR,
51        SqlError::Optimizer(_) => INTERNAL_ERROR,
52        SqlError::AccessDenied { .. } => INSUFFICIENT_PRIVILEGE,
53        SqlError::OperationCancelled { .. } => QUERY_CANCELLED,
54        SqlError::Timeout { .. } => QUERY_TIMEOUT,
55    }
56}
57
58/// A structured error envelope carrying the SQLSTATE code alongside the
59/// original error message.  Suitable for embedding in Flight SQL or JDBC
60/// error responses.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct SqlStateError {
63    /// 5-character SQLSTATE code.
64    pub code: &'static str,
65    /// Human-readable error message.
66    pub message: String,
67}
68
69impl SqlStateError {
70    /// Build a `SqlStateError` from a [`SqlError`].
71    pub fn from_sql_error(error: &SqlError) -> Self {
72        Self {
73            code: sqlstate_for(error),
74            message: error.to_string(),
75        }
76    }
77}
78
79impl std::fmt::Display for SqlStateError {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(f, "SQLSTATE {} — {}", self.code, self.message)
82    }
83}
84
85impl std::error::Error for SqlStateError {}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn empty_query_maps_to_syntax_error() {
93        let e = SqlError::EmptyQuery;
94        assert_eq!(sqlstate_for(&e), SYNTAX_ERROR);
95    }
96
97    #[test]
98    fn unsupported_maps_to_feature_not_supported() {
99        let e = SqlError::Unsupported {
100            feature: "TABLESAMPLE".into(),
101        };
102        assert_eq!(sqlstate_for(&e), FEATURE_NOT_SUPPORTED);
103    }
104
105    #[test]
106    fn datafusion_maps_to_internal_error() {
107        let e = SqlError::DataFusion {
108            message: "panic in executor".into(),
109        };
110        assert_eq!(sqlstate_for(&e), INTERNAL_ERROR);
111    }
112
113    #[test]
114    fn access_denied_maps_to_insufficient_privilege() {
115        let e = SqlError::AccessDenied {
116            reason: "no read permission".into(),
117        };
118        assert_eq!(sqlstate_for(&e), INSUFFICIENT_PRIVILEGE);
119    }
120
121    #[test]
122    fn cancelled_maps_to_query_cancelled() {
123        let e = SqlError::OperationCancelled { operation_id: 42 };
124        assert_eq!(sqlstate_for(&e), QUERY_CANCELLED);
125    }
126
127    #[test]
128    fn timeout_maps_to_query_timeout() {
129        let e = SqlError::Timeout { timeout_ms: 5000 };
130        assert_eq!(sqlstate_for(&e), QUERY_TIMEOUT);
131    }
132
133    #[test]
134    fn sql_state_error_display() {
135        let e = SqlError::EmptyQuery;
136        let se = SqlStateError::from_sql_error(&e);
137        let s = se.to_string();
138        assert!(s.contains(SYNTAX_ERROR));
139        assert!(s.contains("empty"));
140    }
141
142    #[test]
143    fn sql_state_error_is_std_error() {
144        let e = SqlError::EmptyQuery;
145        let se = SqlStateError::from_sql_error(&e);
146        let _: &dyn std::error::Error = &se;
147    }
148
149    #[test]
150    fn all_sqlstate_codes_are_5_chars() {
151        for code in &[
152            SUCCESS,
153            FEATURE_NOT_SUPPORTED,
154            DATA_EXCEPTION,
155            INVALID_AUTHORIZATION,
156            SYNTAX_ERROR,
157            INSUFFICIENT_PRIVILEGE,
158            UNDEFINED_TABLE,
159            QUERY_CANCELLED,
160            QUERY_TIMEOUT,
161            SYSTEM_ERROR,
162            INTERNAL_ERROR,
163            GENERAL_ERROR,
164        ] {
165            assert_eq!(code.len(), 5, "SQLSTATE {code} must be 5 characters");
166        }
167    }
168}