sqlstate_inline/
sqlstate.rs

1// © 2022 Christoph Grenz <https://grenz-bonn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use core::fmt;
6use core::ops::Deref;
7use core::str::FromStr;
8#[cfg(feature = "std")]
9use std::io;
10
11use crate::category::Category;
12use crate::character::Char;
13use crate::class::Class;
14use crate::error::ParseError;
15
16/// Representation for an alphanumeric SQLSTATE code.
17///
18/// This struct can be treated like a 5-byte fixed-length string restricted to `A`-`Z` and `0`-`9`.
19/// It is dereferencable as a [`str`] and can be converted into `[u8; 5]` or `&[u8]` using [`Into`]
20/// and [`AsRef`].
21#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct SqlState {
23	code: [Char; 5],
24}
25
26impl SqlState {
27	/// Creates an `SqlState` from a string.
28	///
29	/// # Errors
30	/// Returns an `Err` if the string isn't of length 5 or contains characters other than `A`-`Z`
31	/// and `0`-`9`.
32	///
33	/// # Examples
34	/// ```
35	/// # use sqlstate_inline::SqlState;
36	/// # fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
37	/// let input = "08001";
38	/// let sqlstate = SqlState::from_str(input)?;
39	///
40	/// assert_eq!(input, &*sqlstate);
41	/// # Ok(())
42	/// # }
43	/// ```
44	#[inline]
45	pub const fn from_str(value: &str) -> Result<Self, ParseError> {
46		Self::from_bytes(value.as_bytes())
47	}
48
49	/// Creates an `SqlState` from a string, replacing invalid characters.
50	///
51	/// SQLSTATEs are strings of length 5 containing only ASCII characters `A`-`Z` and `0`-`9`.
52	/// The first two characters designate the "class" and the remaining three characters designate
53	/// the "subclass". This constructor truncates longer strings, and replaces missing or invalid
54	/// characters by the following rules:
55	///
56	/// - an empty string is always replaced with [`SqlState::UNKNOWN`].
57	/// - else every missing or invalid character is replaced with `X`.
58	///
59	/// # Examples
60	/// ```
61	/// # use sqlstate_inline::SqlState;
62	/// let input = "080@Ä";
63	/// let sqlstate = SqlState::from_str_lossy(input);
64	///
65	/// assert_eq!(SqlState("080XX"), sqlstate);
66	/// ```
67	#[inline]
68	pub const fn from_str_lossy(value: &str) -> Self {
69		Self::from_bytes_lossy(value.as_bytes())
70	}
71
72	/// Creates an `SqlState` from an ASCII byte string.
73	///
74	/// # Errors
75	/// Returns an `Err` if the slice isn't of length 5 or contains ASCII character codes other
76	/// than `A`-`Z` and `0`-`9`.
77	///
78	/// # Examples
79	/// ```
80	/// # use sqlstate_inline::SqlState;
81	/// # fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
82	/// let input = b"08001";
83	/// let sqlstate = SqlState::from_bytes(input)?;
84	///
85	/// assert_eq!(input, AsRef::<[u8]>::as_ref(&sqlstate));
86	/// # Ok(())
87	/// # }
88	/// ```
89	#[inline]
90	pub const fn from_bytes(value: &[u8]) -> Result<Self, ParseError> {
91		match Char::new_array(value) {
92			Ok(code) => Ok(Self { code }),
93			Err(e) => Err(e),
94		}
95	}
96
97	/// Creates an `SqlState` from a byte string, replacing invalid characters.
98	///
99	/// This is the ASCII byte string version of [`SqlState::from_str_lossy()`].
100	#[inline]
101	pub const fn from_bytes_lossy(value: &[u8]) -> Self {
102		if value.is_empty() {
103			Self::UNKNOWN
104		} else {
105			Self {
106				code: Char::new_array_lossy(value, Char::X),
107			}
108		}
109	}
110
111	/// Creates an `SqlState` from an ASCII byte array.
112	///
113	/// # Errors
114	/// Returns an `Err` if the array contains ASCII character codes other than `A`-`Z` and `0`-`9`.
115	///
116	/// # Examples
117	/// ```
118	/// # use sqlstate_inline::SqlState;
119	/// # fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
120	/// let input = [b'4', b'2', b'0', b'0', b'0'];
121	/// let sqlstate = SqlState::from_byte_array(input)?;
122	///
123	/// assert_eq!(&input, AsRef::<[u8; 5]>::as_ref(&sqlstate));
124	/// # Ok(())
125	/// # }
126	/// ```
127	///
128	/// # See also
129	/// [`Class::from_bytes()`]
130	pub const fn from_byte_array(value: [u8; 5]) -> Result<Self, ParseError> {
131		match Char::new_array(&value) {
132			Ok(code) => Ok(Self { code }),
133			Err(e) => Err(e),
134		}
135	}
136
137	/// Returns the general category for this SQLSTATE.
138	///
139	/// # Examples
140	/// ```
141	/// # use sqlstate_inline::{SqlState, Category};
142	/// let sqlstate = SqlState("08001");
143	/// assert_eq!(sqlstate.category(), Category::Exception);
144	/// ```
145	#[inline]
146	pub const fn category(&self) -> Category {
147		match self {
148			sqlstate![0 0 ..] => Category::Success,
149			sqlstate![0 1 ..] => Category::Warning,
150			sqlstate![0 2 ..] => Category::NoData,
151			_ => Category::Exception,
152		}
153	}
154
155	/// Returns the class code for this SQLSTATE.
156	/// ```
157	/// # use sqlstate_inline::{SqlState, Class};
158	/// let sqlstate = SqlState("42000");
159	/// let class = sqlstate.class();
160	///
161	/// assert_eq!(class, Class::SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION);
162	/// assert_eq!(&class, "42");
163	/// ```
164	#[inline]
165	pub const fn class(&self) -> Class {
166		match self.code {
167			[a, b, _, _, _] => Class { code: [a, b] },
168		}
169	}
170
171	/// Returns whether this code is implementation-specific and not reserved for standard conditions.
172	///
173	/// All classes and all subclasses starting with `5`-`9` or `I`-`Z` are implementation-specific.
174	///
175	/// # Examples
176	/// ```
177	/// # use sqlstate_inline::{SqlState, Category};
178	/// let sqlstate = SqlState("42000");
179	/// assert_eq!(sqlstate.is_implementation_specific(), false);
180	///
181	/// let sqlstate = SqlState("XRUST");
182	/// assert_eq!(sqlstate.is_implementation_specific(), true);
183	/// let sqlstate = SqlState("XR000");
184	/// assert_eq!(sqlstate.is_implementation_specific(), true);
185	/// let sqlstate = SqlState("42R42");
186	/// assert_eq!(sqlstate.is_implementation_specific(), true);
187	/// ```
188	#[inline]
189	pub const fn is_implementation_specific(&self) -> bool {
190		!matches!(
191			self.code[0],
192			Char::_0
193				| Char::_1 | Char::_2
194				| Char::_3 | Char::_4
195				| Char::A | Char::B
196				| Char::C | Char::D
197				| Char::E | Char::F
198				| Char::G | Char::H
199		) || !matches!(
200			self.code[2],
201			Char::_0
202				| Char::_1 | Char::_2
203				| Char::_3 | Char::_4
204				| Char::A | Char::B
205				| Char::C | Char::D
206				| Char::E | Char::F
207				| Char::G | Char::H
208		)
209	}
210
211	/// Returns whether the SQLSTATE contains a subclass
212	///
213	/// `false` if the code is a generic class code ending in `000`.
214	#[inline]
215	pub const fn has_subclass(&self) -> bool {
216		!matches!(self.code, [_, _, Char::_0, Char::_0, Char::_0])
217	}
218
219	/// `SqlState("99999")`
220	///
221	/// Used by Oracle for uncategorized errors and also used in this crate
222	/// when an empty string is passed to [`SqlState::from_str_lossy()`].
223	pub const UNKNOWN: Self = sqlstate![9 9 9 9 9];
224}
225
226impl fmt::Debug for SqlState {
227	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
228		f.debug_tuple("SqlState").field(&&**self).finish()
229	}
230}
231
232impl fmt::Display for SqlState {
233	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
234		f.write_str(self)
235	}
236}
237
238impl AsRef<str> for SqlState {
239	#[inline]
240	fn as_ref(&self) -> &str {
241		self
242	}
243}
244
245impl AsRef<[u8]> for SqlState {
246	#[inline]
247	fn as_ref(&self) -> &[u8] {
248		Char::array_as_bytes(&self.code)
249	}
250}
251
252impl AsRef<[u8; 5]> for SqlState {
253	#[inline]
254	fn as_ref(&self) -> &[u8; 5] {
255		Char::array_as_bytes(&self.code)
256	}
257}
258
259impl From<SqlState> for [u8; 5] {
260	#[inline]
261	fn from(sqlstate: SqlState) -> Self {
262		sqlstate.code.map(Char::as_byte)
263	}
264}
265
266impl Deref for SqlState {
267	type Target = str;
268	#[inline]
269	fn deref(&self) -> &Self::Target {
270		Char::array_as_str(&self.code)
271	}
272}
273
274impl PartialEq<str> for SqlState {
275	fn eq(&self, other: &str) -> bool {
276		&**self == other
277	}
278}
279
280impl PartialEq<[u8]> for SqlState {
281	fn eq(&self, other: &[u8]) -> bool {
282		AsRef::<[u8]>::as_ref(self) == other
283	}
284}
285
286#[cfg(feature = "std")]
287impl From<SqlState> for io::ErrorKind {
288	/// Converts the SQLSTATE to an [`std::io::ErrorKind`].
289	///
290	/// Convert well-known codes that have a rough equivalent to the corresponding `ErrorKind` on
291	/// a best-effort basis and falls back to [`ErrorKind::Other`] for everything else. Thus this
292	/// mapping is not considered stable and might change once new `ErrorKind` variants are added.
293	///
294	/// [`ErrorKind::Other`]: `io::ErrorKind::Other`
295	fn from(state: SqlState) -> Self {
296		match state {
297			// 08: Connection errors
298			sqlstate![0 8 0 0 1] | sqlstate![0 8 0 0 4] => Self::ConnectionRefused,
299			sqlstate![0 8 0 0 3] => Self::ConnectionAborted,
300			sqlstate![0 8 ..] => Self::ConnectionReset,
301
302			// 0A: Unsupported
303			sqlstate![0 A ..] => Self::Unsupported,
304
305			// 21: Cardinality error
306			sqlstate![2 1 ..] => Self::InvalidInput,
307
308			// 22: Query data errors
309			sqlstate![2 2 ..] => Self::InvalidData,
310
311			// 23: Constraint violation
312			sqlstate![2 3 5 0 5] => Self::AlreadyExists,
313			sqlstate![2 3 ..] => Self::PermissionDenied,
314
315			// 25: Invalid transaction state
316			//sqlstate![2 5 0 0 6] => Self::ReadOnlyFilesystem,
317			sqlstate![2 5 P 0 3] => Self::TimedOut,
318
319			// 26: Invalid statement name
320			sqlstate![2 6 ..] => Self::InvalidData,
321
322			// 28: Authorization
323			sqlstate![2 8 ..] => Self::PermissionDenied,
324
325			// 40: Transaction rollback
326			//sqlstate![4 0 0 0 1] => Self::ResourceBusy,
327			sqlstate![4 0 0 0 2] => Self::PermissionDenied,
328			//sqlstate![4 0 P 0 1] => Self::Deadlock,
329
330			// 42: Query syntax error or access rule violation
331			sqlstate![4 2 5 ..] => Self::PermissionDenied,
332			sqlstate![4 2 S 0 1] => Self::AlreadyExists,
333			sqlstate![4 2 S 0 2] | sqlstate![4 2 S 1 2] => Self::NotFound,
334			sqlstate![4 2 ..] => Self::InvalidInput,
335
336			// 44: WITH CHECK OPTION violation
337			sqlstate![4 4 ..] => Self::PermissionDenied,
338
339			// 53 (PostgreSQL): Resource exhaustion
340			//sqlstate![5 3 1 0 0] => Self::StorageFull,
341			sqlstate![5 3 2 0 0] => Self::OutOfMemory,
342			sqlstate![5 3 3 0 0] => Self::ConnectionRefused,
343
344			// 54 (PostgreSQL): Program limit exceeded
345			sqlstate![5 4 ..] => Self::InvalidInput,
346
347			// 57 (PostgreSQL): Intervention
348			sqlstate![5 7 0 1 4] => Self::Interrupted,
349			sqlstate![5 7 P 0 1] => Self::ConnectionReset,
350			sqlstate![5 7 P 0 2] => Self::ConnectionReset,
351			sqlstate![5 7 P 0 3] => Self::ConnectionRefused,
352			//sqlstate![5 7 P 0 4] => Self::StaleNetworkFileHandle,
353			sqlstate![5 7 P 0 5] => Self::ConnectionAborted,
354
355			// 70 (MySQL): Interruption
356			sqlstate![7 0 1 0 0] => Self::Interrupted,
357
358			// HV: Foreign data wrappers
359			sqlstate![H V 0 0 1] => Self::OutOfMemory,
360			//sqlstate![H V 0 0 B] => Self::StaleNetworkFileHandle,
361			sqlstate![H V 0 0 N] => Self::BrokenPipe,
362
363			// HY: Call level interface
364			sqlstate![H Y 0 0 1] => Self::OutOfMemory,
365			sqlstate![H Y 0 0 8] => Self::TimedOut,
366
367			// XX (PostgreSQL): Internal Error / Data corruption
368			sqlstate![X X 0 0 1] | sqlstate![X X 0 0 2] => Self::UnexpectedEof,
369
370			_ => Self::Other,
371		}
372	}
373}
374
375impl FromStr for SqlState {
376	type Err = ParseError;
377
378	fn from_str(s: &str) -> Result<Self, Self::Err> {
379		Self::from_bytes(s.as_bytes())
380	}
381}
382
383impl TryFrom<&[u8]> for SqlState {
384	type Error = ParseError;
385
386	fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
387		Self::from_bytes(bytes)
388	}
389}
390
391impl TryFrom<[u8; 5]> for SqlState {
392	type Error = ParseError;
393
394	fn try_from(bytes: [u8; 5]) -> Result<Self, Self::Error> {
395		Self::from_byte_array(bytes)
396	}
397}
398
399impl TryFrom<&str> for SqlState {
400	type Error = ParseError;
401
402	fn try_from(string: &str) -> Result<Self, Self::Error> {
403		Self::from_str(string)
404	}
405}
406
407#[cfg(feature = "serde")]
408impl serde::Serialize for SqlState {
409	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
410	where
411		S: serde::Serializer,
412	{
413		serializer.serialize_str(self)
414	}
415}
416
417#[cfg(feature = "serde")]
418impl<'de> serde::Deserialize<'de> for SqlState {
419	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
420	where
421		D: serde::Deserializer<'de>,
422	{
423		deserializer
424			.deserialize_str(crate::character::de::ArrayVisitor::new())
425			.map(|arr| Self { code: arr })
426	}
427}
428
429// Statically assert the intended memory layouts (5 bytes with multiple niches)
430const _: () = assert!(core::mem::size_of::<SqlState>() == 5);
431const _: () = assert!(core::mem::size_of::<Option<Option<SqlState>>>() == 5);