use std::fmt;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("i/o error: {0}")]
Io(#[from] std::io::Error),
#[error("protocol error: {0}")]
Protocol(String),
#[error("authentication error: {0}")]
Auth(String),
#[error(transparent)]
Database(#[from] DatabaseError),
#[error("conversion error: {0}")]
Conversion(String),
#[error("pool error: {0}")]
Pool(String),
#[error("operation timed out")]
Timeout,
#[error("connection is closed")]
Closed,
#[error("unsupported: {0}")]
Unsupported(String),
}
impl Error {
pub fn protocol(msg: impl Into<String>) -> Self {
Error::Protocol(msg.into())
}
pub fn auth(msg: impl Into<String>) -> Self {
Error::Auth(msg.into())
}
pub fn conversion(msg: impl Into<String>) -> Self {
Error::Conversion(msg.into())
}
pub fn unsupported(msg: impl Into<String>) -> Self {
Error::Unsupported(msg.into())
}
pub fn gds_code(&self) -> Option<i32> {
match self {
Error::Database(db) => db.gds_code(),
_ => None,
}
}
pub fn sql_state(&self) -> Option<&str> {
match self {
Error::Database(db) => db.sql_state.as_deref(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StatusArg {
Gds(i32),
Warning(i32),
Number(i32),
Str(String),
Interpreted(String),
}
#[derive(Debug, Clone)]
pub struct StatusVector {
pub args: Vec<StatusArg>,
pub sql_state: Option<String>,
}
impl StatusVector {
pub fn is_empty(&self) -> bool {
!self
.args
.iter()
.any(|a| matches!(a, StatusArg::Gds(_) | StatusArg::Warning(_)))
}
pub fn is_error(&self) -> bool {
self.args
.iter()
.any(|a| matches!(a, StatusArg::Gds(c) if *c != 0))
}
pub fn gds_code(&self) -> Option<i32> {
self.args.iter().find_map(|a| match a {
StatusArg::Gds(c) if *c != 0 => Some(*c),
_ => None,
})
}
fn message(&self) -> String {
let interpreted: Vec<String> = self
.args
.iter()
.filter_map(|a| match a {
StatusArg::Interpreted(s) if !s.is_empty() => Some(s.clone()),
_ => None,
})
.collect();
if !interpreted.is_empty() {
return interpreted.join("; ");
}
let fill: Vec<String> = self
.args
.iter()
.filter_map(|a| match a {
StatusArg::Number(n) => Some(n.to_string()),
StatusArg::Str(s) => Some(s.clone()),
_ => None,
})
.collect();
let templated: Vec<String> = self
.args
.iter()
.filter_map(|a| match a {
StatusArg::Gds(c) if *c != 0 => gds_template(*c).map(|t| fill_template(t, &fill)),
_ => None,
})
.collect();
if !templated.is_empty() {
return templated.join("; ");
}
if !fill.is_empty() {
return fill.join("; ");
}
match self.gds_code() {
Some(c) => format!("Firebird error (gds code {c})"),
None => "unknown Firebird error".to_string(),
}
}
}
fn fill_template(template: &str, fill: &[String]) -> String {
let mut out = template.to_string();
for i in (1..=fill.len().min(9)).rev() {
out = out.replace(&format!("@{i}"), &fill[i - 1]);
}
out
}
fn gds_template(code: i32) -> Option<&'static str> {
Some(match code {
335544321 => "arithmetic exception, numeric overflow, or string truncation",
335544324 => "invalid database handle (no active connection)",
335544328 => "invalid BLOB handle",
335544329 => "invalid BLOB ID",
335544333 => "internal Firebird consistency check (@1)",
335544334 => "conversion error from string \"@1\"",
335544336 => "deadlock",
335544344 => "I/O error during \"@1\" operation for file \"@2\"",
335544345 => "lock conflict on no wait transaction",
335544347 => "validation error for column @1, value \"@2\"",
335544348 => "no current record for fetch operation",
335544349 => {
"attempt to store duplicate value (visible to active transactions) in unique index \"@1\""
}
335544351 => "unsuccessful metadata update",
335544352 => "no permission for @1 access to @2 @3",
335544359 => "attempted update of read-only column @1",
335544361 => "attempted update during read-only transaction",
335544380 => "wrong number of arguments on call",
335544395 => "table @1 is not defined",
335544396 => "column @1 is not defined in table @2",
335544421 => "connection rejected by remote interface",
335544451 => "update conflicts with concurrent update",
335544466 => "violation of FOREIGN KEY constraint \"@1\" on table \"@2\"",
335544472 => {
"Your user name and password are not defined. Ask your database administrator to set up a Firebird login."
}
335544510 => "lock time-out on wait transaction",
335544558 => "Operation violates CHECK constraint @1 on view or table @2",
335544569 => "Dynamic SQL Error",
335544578 => "Column unknown",
335544580 => "Table unknown",
335544606 => "expression evaluation not supported",
335544634 => "Token unknown - line @1, column @2",
335544665 => "violation of PRIMARY or UNIQUE KEY constraint \"@1\" on table \"@2\"",
_ => return None,
})
}
impl fmt::Display for StatusVector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message())
}
}
#[derive(Debug, Clone)]
pub struct DatabaseError {
pub status: StatusVector,
pub sql_state: Option<String>,
message: String,
}
impl DatabaseError {
pub fn new(status: StatusVector) -> Self {
let message = status.message();
let sql_state = status.sql_state.clone();
DatabaseError {
status,
sql_state,
message,
}
}
pub fn gds_code(&self) -> Option<i32> {
self.status.gds_code()
}
}
impl fmt::Display for DatabaseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (&self.sql_state, self.gds_code()) {
(Some(state), Some(code)) => {
write!(f, "{} (SQLSTATE {state}, gds {code})", self.message)
}
(None, Some(code)) => write!(f, "{} (gds {code})", self.message),
_ => f.write_str(&self.message),
}
}
}
impl std::error::Error for DatabaseError {}
impl From<StatusVector> for Error {
fn from(status: StatusVector) -> Self {
Error::Database(DatabaseError::new(status))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sv(args: Vec<StatusArg>) -> StatusVector {
StatusVector {
args,
sql_state: None,
}
}
#[test]
fn templated_message_fills_placeholders() {
let v = sv(vec![
StatusArg::Gds(335544466),
StatusArg::Str("FK_PEDIDO_CLIENTE".into()),
StatusArg::Str("PEDIDO".into()),
]);
assert_eq!(
v.message(),
"violation of FOREIGN KEY constraint \"FK_PEDIDO_CLIENTE\" on table \"PEDIDO\""
);
}
#[test]
fn chained_gds_codes_are_joined() {
let v = sv(vec![
StatusArg::Gds(335544569),
StatusArg::Gds(335544634),
StatusArg::Number(1),
StatusArg::Number(42),
]);
assert_eq!(
v.message(),
"Dynamic SQL Error; Token unknown - line 1, column 42"
);
}
#[test]
fn interpreted_text_wins() {
let v = sv(vec![
StatusArg::Gds(335544321),
StatusArg::Interpreted("texto do servidor".into()),
]);
assert_eq!(v.message(), "texto do servidor");
}
#[test]
fn unknown_code_falls_back_to_number() {
let v = sv(vec![StatusArg::Gds(999999)]);
assert_eq!(v.message(), "Firebird error (gds code 999999)");
assert!(v.is_error());
}
#[test]
fn deadlock_has_no_placeholders() {
assert_eq!(sv(vec![StatusArg::Gds(335544336)]).message(), "deadlock");
}
}