use std::fmt;
use crate::generated::sqlstate::{CLASSES, CODES};
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SqlState([u8; 5]);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Category {
Success,
Warning,
Error,
}
impl SqlState {
pub(crate) const fn from_bytes(code: [u8; 5]) -> Self {
Self(code)
}
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
let bytes: [u8; 5] = text.as_bytes().try_into().ok()?;
bytes.iter().all(|b| b.is_ascii_digit() || b.is_ascii_uppercase()).then_some(Self(bytes))
}
#[must_use]
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.0).unwrap_or("XX000")
}
#[must_use]
pub fn class(&self) -> &str {
&self.as_str()[..2]
}
#[must_use]
pub fn class_title(self) -> Option<&'static str> {
let class = self.class();
CLASSES.iter().find(|(code, _)| *code == class).map(|(_, title)| *title)
}
#[must_use]
pub fn category(self) -> Category {
match self.class() {
"00" => Category::Success,
"01" | "02" => Category::Warning,
_ => Category::Error,
}
}
#[must_use]
pub fn name(self) -> Option<&'static str> {
CODES.iter().find(|(state, ..)| *state == self).map(|(_, _, name, _)| *name)
}
#[must_use]
pub fn condition(self) -> Option<&'static str> {
CODES
.iter()
.find(|(state, _, _, condition)| *state == self && !condition.is_empty())
.map(|(_, _, _, condition)| *condition)
}
pub fn for_condition(name: &str) -> impl Iterator<Item = Self> + '_ {
CODES.iter().filter(move |(.., condition)| *condition == name).map(|(state, ..)| *state)
}
pub fn all() -> impl Iterator<Item = (Self, &'static str)> {
CODES.iter().map(|(state, _, name, _)| (*state, *name))
}
}
impl fmt::Display for SqlState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Debug for SqlState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SqlState({})", self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::{Category, SqlState};
#[test]
fn the_list_has_every_code_line_of_the_pin() {
assert_eq!(SqlState::all().count(), 268);
let mut distinct: Vec<SqlState> = SqlState::all().map(|(state, _)| state).collect();
distinct.sort();
distinct.dedup();
assert_eq!(distinct.len(), 262);
}
#[test]
fn a_constant_is_its_code() {
assert_eq!(SqlState::UNDEFINED_TABLE.as_str(), "42P01");
assert_eq!(SqlState::UNDEFINED_COLUMN.to_string(), "42703");
assert_eq!(SqlState::FEATURE_NOT_SUPPORTED.as_str(), "0A000");
assert_eq!(SqlState::T_R_SERIALIZATION_FAILURE.as_str(), "40001");
assert_eq!(SqlState::INTERNAL_ERROR.as_str(), "XX000");
assert_eq!(format!("{:?}", SqlState::SYNTAX_ERROR), "SqlState(42601)");
}
#[test]
fn a_second_name_is_the_same_code() {
assert_eq!(SqlState::UNDEFINED_DATABASE, SqlState::INVALID_CATALOG_NAME);
assert_eq!(SqlState::UNDEFINED_DATABASE.name(), Some("INVALID_CATALOG_NAME"));
assert_eq!(SqlState::UNDEFINED_DATABASE.condition(), Some("invalid_catalog_name"));
}
#[test]
fn a_condition_name_finds_every_code_that_has_it() {
let found: Vec<String> = SqlState::for_condition("string_data_right_truncation")
.map(|s| s.to_string())
.collect();
assert_eq!(found, ["01004", "22001"]);
assert_eq!(SqlState::for_condition("no_such_condition").count(), 0);
assert_eq!(SqlState::UNIQUE_VIOLATION.condition(), Some("unique_violation"));
}
#[test]
fn the_class_gives_the_category_and_the_title() {
assert_eq!(SqlState::UNDEFINED_TABLE.class(), "42");
assert_eq!(
SqlState::UNDEFINED_TABLE.class_title(),
Some("Syntax Error or Access Rule Violation")
);
assert_eq!(SqlState::SUCCESSFUL_COMPLETION.category(), Category::Success);
assert_eq!(SqlState::NO_DATA.category(), Category::Warning);
assert_eq!(SqlState::DIVISION_BY_ZERO.category(), Category::Error);
}
#[test]
fn parse_takes_any_well_formed_code() {
assert_eq!(SqlState::parse("42P01"), Some(SqlState::UNDEFINED_TABLE));
let custom = SqlState::parse("P9999").expect("a well formed code");
assert_eq!(custom.name(), None);
assert_eq!(custom.category(), Category::Error);
assert_eq!(SqlState::parse("42p01"), None);
assert_eq!(SqlState::parse("4201"), None);
assert_eq!(SqlState::parse("42P01X"), None);
}
#[test]
fn the_generated_category_agrees_with_the_class() {
for (state, category, _, _) in &crate::generated::sqlstate::CODES {
assert_eq!(state.category(), *category, "{state}");
}
}
}