Skip to main content

firebird_wire/
error.rs

1//! Tipos de erro do driver.
2//!
3//! Falhas do lado do servidor chegam como um *status vector* do Firebird: uma
4//! sequência de argumentos marcados (códigos de erro, números e strings). Nós o
5//! parseamos em um [`StatusVector`] estruturado e expomos o código GDS primário
6//! e o SQLSTATE para que os chamadores possam fazer match em condições
7//! específicas sem ter que vasculhar strings.
8
9use std::fmt;
10
11/// Alias de conveniência usado em toda a crate.
12pub type Result<T> = std::result::Result<T, Error>;
13
14/// O tipo de erro de nível mais alto.
15#[derive(Debug, thiserror::Error)]
16pub enum Error {
17    /// Falha de I/O no nível de transporte.
18    #[error("i/o error: {0}")]
19    Io(#[from] std::io::Error),
20
21    /// O par enviou algo que viola o protocolo de comunicação (wire protocol).
22    #[error("protocol error: {0}")]
23    Protocol(String),
24
25    /// Falha de autenticação/handshake (SRP, wire crypt, incompatibilidade de plugin).
26    #[error("authentication error: {0}")]
27    Auth(String),
28
29    /// Um erro reportado pelo servidor com um status vector completo.
30    #[error(transparent)]
31    Database(#[from] DatabaseError),
32
33    /// Um valor não pôde ser convertido de/para o tipo Rust solicitado.
34    #[error("conversion error: {0}")]
35    Conversion(String),
36
37    /// Falha no pool de conexões (esgotado, fechado, ...).
38    #[error("pool error: {0}")]
39    Pool(String),
40
41    /// Uma operação excedeu seu prazo.
42    #[error("operation timed out")]
43    Timeout,
44
45    /// A conexão foi fechada e não pode mais ser usada.
46    #[error("connection is closed")]
47    Closed,
48
49    /// Um recurso ainda não é suportado pelo protocolo negociado ou por este driver.
50    #[error("unsupported: {0}")]
51    Unsupported(String),
52}
53
54impl Error {
55    /// Cria um erro de protocolo para validações internas do driver.
56    pub fn protocol(msg: impl Into<String>) -> Self {
57        Error::Protocol(msg.into())
58    }
59    /// Cria um erro de autenticação ou negociação de criptografia.
60    pub fn auth(msg: impl Into<String>) -> Self {
61        Error::Auth(msg.into())
62    }
63    /// Cria um erro de conversão de valor.
64    pub fn conversion(msg: impl Into<String>) -> Self {
65        Error::Conversion(msg.into())
66    }
67    /// Cria um erro para recurso ainda não suportado.
68    pub fn unsupported(msg: impl Into<String>) -> Self {
69        Error::Unsupported(msg.into())
70    }
71
72    /// Se este for um erro de banco de dados, o código de erro primário do Firebird (GDS).
73    pub fn gds_code(&self) -> Option<i32> {
74        match self {
75            Error::Database(db) => db.gds_code(),
76            _ => None,
77        }
78    }
79
80    /// Se este for um erro de banco de dados, seu SQLSTATE (5 caracteres), quando fornecido.
81    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/// Um elemento de um status vector do Firebird.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum StatusArg {
92    /// Um código de erro Firebird/GDS (`isc_arg_gds`).
93    Gds(i32),
94    /// Um código de aviso (`isc_arg_warning`).
95    Warning(i32),
96    /// Um argumento numérico usado para preencher um placeholder da mensagem.
97    Number(i32),
98    /// Um argumento de string usado para preencher um placeholder da mensagem.
99    Str(String),
100    /// Texto que o servidor já interpretou para nós.
101    Interpreted(String),
102}
103
104/// Um status vector parseado mais uma mensagem legível por humanos com melhor esforço.
105#[derive(Debug, Clone)]
106pub struct StatusVector {
107    /// Argumentos crus enviados pelo Firebird no status vector.
108    pub args: Vec<StatusArg>,
109    /// SQLSTATE de 5 caracteres, quando o servidor informa.
110    pub sql_state: Option<String>,
111}
112
113impl StatusVector {
114    /// Verdadeiro quando o vetor não carrega nenhum código de erro.
115    pub fn is_empty(&self) -> bool {
116        !self
117            .args
118            .iter()
119            .any(|a| matches!(a, StatusArg::Gds(_) | StatusArg::Warning(_)))
120    }
121
122    /// Verdadeiro quando o vetor representa uma falha real. Um `isc_arg_gds` com
123    /// código `0` é o sentinela de "success" do Firebird e *não* é um erro.
124    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    /// O primeiro código de erro GDS diferente de zero, se houver.
131    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    /// Constrói uma mensagem com melhor esforço. Não embutimos o catálogo
139    /// `firebird.msg` completo, mas temos um mapa dos códigos GDS mais comuns
140    /// ([`gds_template`]) cujos placeholders `@N` preenchemos com os argumentos do
141    /// status vector. A ordem de preferência é: (1) texto já interpretado pelo
142    /// servidor; (2) templates conhecidos formatados; (3) argumentos crus; (4) o
143    /// código GDS bruto.
144    fn message(&self) -> String {
145        // (1) O servidor às vezes já manda o texto pronto.
146        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        // Valores que preenchem @1, @2, … na ordem em que aparecem.
159        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        // (2) Formata cada código GDS com template conhecido.
170        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        // (3)/(4) Argumentos crus, senão o código.
183        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
193/// Substitui os marcadores `@1`..`@9` de um template do Firebird pelos valores em
194/// `fill` (na ordem). Marcadores sem valor correspondente ficam como estão.
195fn fill_template(template: &str, fill: &[String]) -> String {
196    let mut out = template.to_string();
197    // Em ordem decrescente para que `@1` não case dentro de um `@10` (inexistente
198    // nos templates atuais, mas a ordem é barata e à prova de futuro).
199    for i in (1..=fill.len().min(9)).rev() {
200        out = out.replace(&format!("@{i}"), &fill[i - 1]);
201    }
202    out
203}
204
205/// Texto dos códigos de erro GDS mais comuns (extraído do `firebird.msg` do FB5
206/// via `fb_interpret`). Os `@N` são preenchidos por [`fill_template`]. Cobre os
207/// erros que aplicações realmente encontram; o resto recai no código bruto.
208fn 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/// Um erro do servidor: o status vector e sua mensagem renderizada.
256#[derive(Debug, Clone)]
257pub struct DatabaseError {
258    /// Status vector estruturado enviado pelo Firebird.
259    pub status: StatusVector,
260    /// SQLSTATE de 5 caracteres, quando disponível.
261    pub sql_state: Option<String>,
262    message: String,
263}
264
265impl DatabaseError {
266    /// Cria um erro de banco a partir de um status vector já parseado.
267    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    /// O primeiro código GDS diferente de zero no status vector.
278    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        // FK: violation of FOREIGN KEY constraint "@1" on table "@2".
317        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        // Dynamic SQL Error (sem args) + Token unknown - line @1, column @2.
331        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}