1use std::fmt;
10
11use crate::generated::sqlstate::{CLASSES, CODES};
12
13#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub struct SqlState([u8; 5]);
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum Category {
22 Success,
24 Warning,
26 Error,
28}
29
30impl SqlState {
31 pub(crate) const fn from_bytes(code: [u8; 5]) -> Self {
33 Self(code)
34 }
35
36 #[must_use]
38 pub fn parse(text: &str) -> Option<Self> {
39 let bytes: [u8; 5] = text.as_bytes().try_into().ok()?;
40 bytes.iter().all(|b| b.is_ascii_digit() || b.is_ascii_uppercase()).then_some(Self(bytes))
41 }
42
43 #[must_use]
45 pub fn as_str(&self) -> &str {
46 std::str::from_utf8(&self.0).unwrap_or("XX000")
48 }
49
50 #[must_use]
52 pub fn class(&self) -> &str {
53 &self.as_str()[..2]
54 }
55
56 #[must_use]
58 pub fn class_title(self) -> Option<&'static str> {
59 let class = self.class();
60 CLASSES.iter().find(|(code, _)| *code == class).map(|(_, title)| *title)
61 }
62
63 #[must_use]
65 pub fn category(self) -> Category {
66 match self.class() {
67 "00" => Category::Success,
68 "01" | "02" => Category::Warning,
69 _ => Category::Error,
70 }
71 }
72
73 #[must_use]
75 pub fn name(self) -> Option<&'static str> {
76 CODES.iter().find(|(state, ..)| *state == self).map(|(_, _, name, _)| *name)
77 }
78
79 #[must_use]
81 pub fn condition(self) -> Option<&'static str> {
82 CODES
83 .iter()
84 .find(|(state, _, _, condition)| *state == self && !condition.is_empty())
85 .map(|(_, _, _, condition)| *condition)
86 }
87
88 pub fn for_condition(name: &str) -> impl Iterator<Item = Self> + '_ {
90 CODES.iter().filter(move |(.., condition)| *condition == name).map(|(state, ..)| *state)
91 }
92
93 pub fn all() -> impl Iterator<Item = (Self, &'static str)> {
95 CODES.iter().map(|(state, _, name, _)| (*state, *name))
96 }
97}
98
99impl fmt::Display for SqlState {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.write_str(self.as_str())
102 }
103}
104
105impl fmt::Debug for SqlState {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 write!(f, "SqlState({})", self.as_str())
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::{Category, SqlState};
114
115 #[test]
116 fn the_list_has_every_code_line_of_the_pin() {
117 assert_eq!(SqlState::all().count(), 268);
118 let mut distinct: Vec<SqlState> = SqlState::all().map(|(state, _)| state).collect();
119 distinct.sort();
120 distinct.dedup();
121 assert_eq!(distinct.len(), 262);
122 }
123
124 #[test]
125 fn a_constant_is_its_code() {
126 assert_eq!(SqlState::UNDEFINED_TABLE.as_str(), "42P01");
127 assert_eq!(SqlState::UNDEFINED_COLUMN.to_string(), "42703");
128 assert_eq!(SqlState::FEATURE_NOT_SUPPORTED.as_str(), "0A000");
129 assert_eq!(SqlState::T_R_SERIALIZATION_FAILURE.as_str(), "40001");
130 assert_eq!(SqlState::INTERNAL_ERROR.as_str(), "XX000");
131 assert_eq!(format!("{:?}", SqlState::SYNTAX_ERROR), "SqlState(42601)");
132 }
133
134 #[test]
135 fn a_second_name_is_the_same_code() {
136 assert_eq!(SqlState::UNDEFINED_DATABASE, SqlState::INVALID_CATALOG_NAME);
137 assert_eq!(SqlState::UNDEFINED_DATABASE.name(), Some("INVALID_CATALOG_NAME"));
138 assert_eq!(SqlState::UNDEFINED_DATABASE.condition(), Some("invalid_catalog_name"));
139 }
140
141 #[test]
142 fn a_condition_name_finds_every_code_that_has_it() {
143 let found: Vec<String> = SqlState::for_condition("string_data_right_truncation")
144 .map(|s| s.to_string())
145 .collect();
146 assert_eq!(found, ["01004", "22001"]);
147 assert_eq!(SqlState::for_condition("no_such_condition").count(), 0);
148 assert_eq!(SqlState::UNIQUE_VIOLATION.condition(), Some("unique_violation"));
149 }
150
151 #[test]
152 fn the_class_gives_the_category_and_the_title() {
153 assert_eq!(SqlState::UNDEFINED_TABLE.class(), "42");
154 assert_eq!(
155 SqlState::UNDEFINED_TABLE.class_title(),
156 Some("Syntax Error or Access Rule Violation")
157 );
158 assert_eq!(SqlState::SUCCESSFUL_COMPLETION.category(), Category::Success);
159 assert_eq!(SqlState::NO_DATA.category(), Category::Warning);
160 assert_eq!(SqlState::DIVISION_BY_ZERO.category(), Category::Error);
161 }
162
163 #[test]
164 fn parse_takes_any_well_formed_code() {
165 assert_eq!(SqlState::parse("42P01"), Some(SqlState::UNDEFINED_TABLE));
166 let custom = SqlState::parse("P9999").expect("a well formed code");
167 assert_eq!(custom.name(), None);
168 assert_eq!(custom.category(), Category::Error);
169 assert_eq!(SqlState::parse("42p01"), None);
170 assert_eq!(SqlState::parse("4201"), None);
171 assert_eq!(SqlState::parse("42P01X"), None);
172 }
173
174 #[test]
175 fn the_generated_category_agrees_with_the_class() {
176 for (state, category, _, _) in &crate::generated::sqlstate::CODES {
177 assert_eq!(state.category(), *category, "{state}");
178 }
179 }
180}