Skip to main content

rupg_common/
sqlstate.rs

1//! SQLSTATE codes, the five characters that a PostgreSQL client reads to know what failed.
2//!
3//! Drivers, ORMs and retry loops branch on the code and not on the message. tokio-postgres falls back to an older catalog query on `42P01` and `42703`, Rails prepares a statement again on `0A000`, and every retry loop looks for `40001` and `40P01`. So the code of an error is part of the interface (spec/04 section 4.8).
4//!
5//! `cargo xtask errcodes` makes the list in [`generated::sqlstate`](crate::generated) from `vendor/postgres-19/src/backend/utils/errcodes.txt`. It has one constant for each code line of the file, named as the C macro without the `ERRCODE_` prefix, for example [`SqlState::UNDEFINED_TABLE`]. The file has 268 code lines and 262 distinct codes, because six codes have a second macro name.
6//!
7//! Lifted from `crates/rudb-common/src/sqlstate.rs` of tamnd/rudb at f5f7065a (spec/04 section 4.9).
8
9use std::fmt;
10
11use crate::generated::sqlstate::{CLASSES, CODES};
12
13/// A SQLSTATE: five ASCII characters, each a digit or an upper case letter.
14///
15/// The first two characters are the class. A client that does not know a code can still act on its class, which is why a code that rupg cannot give exactly must at least be in the right class.
16#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub struct SqlState([u8; 5]);
18
19/// What a code means for the statement, from the second column of `errcodes.txt`.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum Category {
22    /// Class `00`. The statement succeeded.
23    Success,
24    /// Classes `01` and `02`. The statement succeeded and the server has something to say.
25    Warning,
26    /// Every other class. The statement failed.
27    Error,
28}
29
30impl SqlState {
31    /// The code with these five bytes. Only the generated list and [`SqlState::parse`] call it, so every value is a valid code.
32    pub(crate) const fn from_bytes(code: [u8; 5]) -> Self {
33        Self(code)
34    }
35
36    /// Reads a code from text. Any five digits or upper case letters make a code, also one that is not in the list, because a client can send any code in `RAISE ... USING ERRCODE`.
37    #[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    /// The code as text, for example `42P01`.
44    #[must_use]
45    pub fn as_str(&self) -> &str {
46        // The constructors accept ASCII only, so this cannot fail.
47        std::str::from_utf8(&self.0).unwrap_or("XX000")
48    }
49
50    /// The class, which is the first two characters, for example `42`.
51    #[must_use]
52    pub fn class(&self) -> &str {
53        &self.as_str()[..2]
54    }
55
56    /// The title of the class in `errcodes.txt`, for example `Syntax Error or Access Rule Violation`. `None` for a class that the file does not have.
57    #[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    /// Whether the statement succeeded, succeeded with a warning or failed. A code that is not in the list takes the category of its class, as in PostgreSQL.
64    #[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    /// The name of the constant for this code, for example `UNDEFINED_TABLE`. When a code has two names, this is the first one in the file. `None` for a code that is not in the list.
74    #[must_use]
75    pub fn name(self) -> Option<&'static str> {
76        CODES.iter().find(|(state, ..)| *state == self).map(|(_, _, name, _)| *name)
77    }
78
79    /// The PL/pgSQL condition name, for example `undefined_table`. `None` for a code that is not in the list.
80    #[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    /// Every code with this PL/pgSQL condition name. Most names have one code. Five names have two, for example `string_data_right_truncation` is both `01004` and `22001`, and an exception handler for the name catches both.
89    pub fn for_condition(name: &str) -> impl Iterator<Item = Self> + '_ {
90        CODES.iter().filter(move |(.., condition)| *condition == name).map(|(state, ..)| *state)
91    }
92
93    /// Every code line of `errcodes.txt` in file order, as the code and the constant name.
94    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}