1use std::fmt;
10
11pub type Result<T> = std::result::Result<T, Error>;
13
14#[derive(Debug, thiserror::Error)]
16pub enum Error {
17 #[error("i/o error: {0}")]
19 Io(#[from] std::io::Error),
20
21 #[error("protocol error: {0}")]
23 Protocol(String),
24
25 #[error("authentication error: {0}")]
27 Auth(String),
28
29 #[error(transparent)]
31 Database(#[from] DatabaseError),
32
33 #[error("conversion error: {0}")]
35 Conversion(String),
36
37 #[error("pool error: {0}")]
39 Pool(String),
40
41 #[error("operation timed out")]
43 Timeout,
44
45 #[error("connection is closed")]
47 Closed,
48
49 #[error("unsupported: {0}")]
51 Unsupported(String),
52}
53
54impl Error {
55 pub fn protocol(msg: impl Into<String>) -> Self {
57 Error::Protocol(msg.into())
58 }
59 pub fn auth(msg: impl Into<String>) -> Self {
61 Error::Auth(msg.into())
62 }
63 pub fn conversion(msg: impl Into<String>) -> Self {
65 Error::Conversion(msg.into())
66 }
67 pub fn unsupported(msg: impl Into<String>) -> Self {
69 Error::Unsupported(msg.into())
70 }
71
72 pub fn gds_code(&self) -> Option<i32> {
74 match self {
75 Error::Database(db) => db.gds_code(),
76 _ => None,
77 }
78 }
79
80 pub fn sql_state(&self) -> Option<&str> {
82 match self {
83 Error::Database(db) => db.sql_state.as_deref(),
84 _ => None,
85 }
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum StatusArg {
92 Gds(i32),
94 Warning(i32),
96 Number(i32),
98 Str(String),
100 Interpreted(String),
102}
103
104#[derive(Debug, Clone)]
106pub struct StatusVector {
107 pub args: Vec<StatusArg>,
109 pub sql_state: Option<String>,
111}
112
113impl StatusVector {
114 pub fn is_empty(&self) -> bool {
116 !self
117 .args
118 .iter()
119 .any(|a| matches!(a, StatusArg::Gds(_) | StatusArg::Warning(_)))
120 }
121
122 pub fn is_error(&self) -> bool {
125 self.args
126 .iter()
127 .any(|a| matches!(a, StatusArg::Gds(c) if *c != 0))
128 }
129
130 pub fn gds_code(&self) -> Option<i32> {
132 self.args.iter().find_map(|a| match a {
133 StatusArg::Gds(c) if *c != 0 => Some(*c),
134 _ => None,
135 })
136 }
137
138 fn message(&self) -> String {
145 let interpreted: Vec<String> = self
147 .args
148 .iter()
149 .filter_map(|a| match a {
150 StatusArg::Interpreted(s) if !s.is_empty() => Some(s.clone()),
151 _ => None,
152 })
153 .collect();
154 if !interpreted.is_empty() {
155 return interpreted.join("; ");
156 }
157
158 let fill: Vec<String> = self
160 .args
161 .iter()
162 .filter_map(|a| match a {
163 StatusArg::Number(n) => Some(n.to_string()),
164 StatusArg::Str(s) => Some(s.clone()),
165 _ => None,
166 })
167 .collect();
168
169 let templated: Vec<String> = self
171 .args
172 .iter()
173 .filter_map(|a| match a {
174 StatusArg::Gds(c) if *c != 0 => gds_template(*c).map(|t| fill_template(t, &fill)),
175 _ => None,
176 })
177 .collect();
178 if !templated.is_empty() {
179 return templated.join("; ");
180 }
181
182 if !fill.is_empty() {
184 return fill.join("; ");
185 }
186 match self.gds_code() {
187 Some(c) => format!("Firebird error (gds code {c})"),
188 None => "unknown Firebird error".to_string(),
189 }
190 }
191}
192
193fn fill_template(template: &str, fill: &[String]) -> String {
196 let mut out = template.to_string();
197 for i in (1..=fill.len().min(9)).rev() {
200 out = out.replace(&format!("@{i}"), &fill[i - 1]);
201 }
202 out
203}
204
205fn gds_template(code: i32) -> Option<&'static str> {
209 Some(match code {
210 335544321 => "arithmetic exception, numeric overflow, or string truncation",
211 335544324 => "invalid database handle (no active connection)",
212 335544328 => "invalid BLOB handle",
213 335544329 => "invalid BLOB ID",
214 335544333 => "internal Firebird consistency check (@1)",
215 335544334 => "conversion error from string \"@1\"",
216 335544336 => "deadlock",
217 335544344 => "I/O error during \"@1\" operation for file \"@2\"",
218 335544345 => "lock conflict on no wait transaction",
219 335544347 => "validation error for column @1, value \"@2\"",
220 335544348 => "no current record for fetch operation",
221 335544349 => {
222 "attempt to store duplicate value (visible to active transactions) in unique index \"@1\""
223 }
224 335544351 => "unsuccessful metadata update",
225 335544352 => "no permission for @1 access to @2 @3",
226 335544359 => "attempted update of read-only column @1",
227 335544361 => "attempted update during read-only transaction",
228 335544380 => "wrong number of arguments on call",
229 335544395 => "table @1 is not defined",
230 335544396 => "column @1 is not defined in table @2",
231 335544421 => "connection rejected by remote interface",
232 335544451 => "update conflicts with concurrent update",
233 335544466 => "violation of FOREIGN KEY constraint \"@1\" on table \"@2\"",
234 335544472 => {
235 "Your user name and password are not defined. Ask your database administrator to set up a Firebird login."
236 }
237 335544510 => "lock time-out on wait transaction",
238 335544558 => "Operation violates CHECK constraint @1 on view or table @2",
239 335544569 => "Dynamic SQL Error",
240 335544578 => "Column unknown",
241 335544580 => "Table unknown",
242 335544606 => "expression evaluation not supported",
243 335544634 => "Token unknown - line @1, column @2",
244 335544665 => "violation of PRIMARY or UNIQUE KEY constraint \"@1\" on table \"@2\"",
245 _ => return None,
246 })
247}
248
249impl fmt::Display for StatusVector {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 f.write_str(&self.message())
252 }
253}
254
255#[derive(Debug, Clone)]
257pub struct DatabaseError {
258 pub status: StatusVector,
260 pub sql_state: Option<String>,
262 message: String,
263}
264
265impl DatabaseError {
266 pub fn new(status: StatusVector) -> Self {
268 let message = status.message();
269 let sql_state = status.sql_state.clone();
270 DatabaseError {
271 status,
272 sql_state,
273 message,
274 }
275 }
276
277 pub fn gds_code(&self) -> Option<i32> {
279 self.status.gds_code()
280 }
281}
282
283impl fmt::Display for DatabaseError {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 match (&self.sql_state, self.gds_code()) {
286 (Some(state), Some(code)) => {
287 write!(f, "{} (SQLSTATE {state}, gds {code})", self.message)
288 }
289 (None, Some(code)) => write!(f, "{} (gds {code})", self.message),
290 _ => f.write_str(&self.message),
291 }
292 }
293}
294
295impl std::error::Error for DatabaseError {}
296
297impl From<StatusVector> for Error {
298 fn from(status: StatusVector) -> Self {
299 Error::Database(DatabaseError::new(status))
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 fn sv(args: Vec<StatusArg>) -> StatusVector {
308 StatusVector {
309 args,
310 sql_state: None,
311 }
312 }
313
314 #[test]
315 fn templated_message_fills_placeholders() {
316 let v = sv(vec![
318 StatusArg::Gds(335544466),
319 StatusArg::Str("FK_PEDIDO_CLIENTE".into()),
320 StatusArg::Str("PEDIDO".into()),
321 ]);
322 assert_eq!(
323 v.message(),
324 "violation of FOREIGN KEY constraint \"FK_PEDIDO_CLIENTE\" on table \"PEDIDO\""
325 );
326 }
327
328 #[test]
329 fn chained_gds_codes_are_joined() {
330 let v = sv(vec![
332 StatusArg::Gds(335544569),
333 StatusArg::Gds(335544634),
334 StatusArg::Number(1),
335 StatusArg::Number(42),
336 ]);
337 assert_eq!(
338 v.message(),
339 "Dynamic SQL Error; Token unknown - line 1, column 42"
340 );
341 }
342
343 #[test]
344 fn interpreted_text_wins() {
345 let v = sv(vec![
346 StatusArg::Gds(335544321),
347 StatusArg::Interpreted("texto do servidor".into()),
348 ]);
349 assert_eq!(v.message(), "texto do servidor");
350 }
351
352 #[test]
353 fn unknown_code_falls_back_to_number() {
354 let v = sv(vec![StatusArg::Gds(999999)]);
355 assert_eq!(v.message(), "Firebird error (gds code 999999)");
356 assert!(v.is_error());
357 }
358
359 #[test]
360 fn deadlock_has_no_placeholders() {
361 assert_eq!(sv(vec![StatusArg::Gds(335544336)]).message(), "deadlock");
362 }
363}