uqa_client/
sql_diagnostic.rs1use std::fmt;
8
9use serde::{Deserialize, Serialize};
10
11use crate::SQLDiagnosticCategory;
12
13#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
15#[serde(try_from = "WireSQLDiagnostic", into = "WireSQLDiagnostic")]
16pub struct SQLDiagnostic {
17 sqlstate: Option<[u8; 5]>,
18 pub category: SQLDiagnosticCategory,
19 pub statement_index: Option<u32>,
21 pub position: Option<u32>,
23}
24
25#[derive(Deserialize, Serialize)]
26#[serde(deny_unknown_fields)]
27struct WireSQLDiagnostic {
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 sqlstate: Option<String>,
30 category: SQLDiagnosticCategory,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 statement_index: Option<u32>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 position: Option<u32>,
35}
36
37impl SQLDiagnostic {
38 pub fn new(
39 sqlstate: Option<&str>,
40 category: SQLDiagnosticCategory,
41 statement_index: Option<u32>,
42 position: Option<u32>,
43 ) -> Self {
44 Self {
45 sqlstate: sqlstate.and_then(valid_sqlstate),
46 category,
47 statement_index,
48 position: position.filter(|value| *value != 0),
49 }
50 }
51
52 pub fn sqlstate(&self) -> Option<&str> {
53 self.sqlstate
54 .as_ref()
55 .and_then(|value| std::str::from_utf8(value).ok())
56 }
57
58 pub fn matches_code(&self, code: &str) -> bool {
59 code == "SQL_EXECUTION_FAILED"
60 }
61}
62
63impl TryFrom<WireSQLDiagnostic> for SQLDiagnostic {
64 type Error = &'static str;
65
66 fn try_from(wire: WireSQLDiagnostic) -> Result<Self, Self::Error> {
67 let sqlstate = wire
68 .sqlstate
69 .as_deref()
70 .map(|value| valid_sqlstate(value).ok_or("invalid SQLSTATE"))
71 .transpose()?;
72 if wire.position == Some(0) {
73 return Err("invalid SQL position");
74 }
75 Ok(Self {
76 sqlstate,
77 category: wire.category,
78 statement_index: wire.statement_index,
79 position: wire.position,
80 })
81 }
82}
83
84impl From<SQLDiagnostic> for WireSQLDiagnostic {
85 fn from(value: SQLDiagnostic) -> Self {
86 Self {
87 sqlstate: value.sqlstate().map(str::to_owned),
88 category: value.category,
89 statement_index: value.statement_index,
90 position: value.position,
91 }
92 }
93}
94
95fn valid_sqlstate(value: &str) -> Option<[u8; 5]> {
96 (value != "00000"
97 && value
98 .bytes()
99 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit()))
100 .then(|| value.as_bytes().try_into().ok())
101 .flatten()
102}
103
104impl fmt::Display for SQLDiagnostic {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(formatter, "{}", self.category.as_str())?;
107 if let Some(sqlstate) = self.sqlstate() {
108 write!(formatter, "; SQLSTATE {sqlstate}")?;
109 }
110 if let Some(index) = self.statement_index {
111 write!(
112 formatter,
113 "; batch statement {} (index {index})",
114 u64::from(index) + 1
115 )?;
116 }
117 if let Some(position) = self.position {
118 write!(formatter, "; character {position}")?;
119 }
120 if let Some(hint) = self.category.hint() {
121 write!(formatter, "; {hint}")?;
122 }
123 Ok(())
124 }
125}