use crate::protocol::backend::{ErrorResponseBody, NoticeResponseBody};
use fallible_iterator::FallibleIterator;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct PgServerError {
pub severity: String,
pub severity_v: Option<String>,
pub code: String,
pub message: String,
pub detail: Option<String>,
pub hint: Option<String>,
pub position: Option<u32>,
pub internal_position: Option<u32>,
pub internal_query: Option<String>,
pub where_: Option<String>,
pub schema: Option<String>,
pub table: Option<String>,
pub column: Option<String>,
pub data_type: Option<String>,
pub constraint: Option<String>,
pub file: Option<String>,
pub line: Option<u32>,
pub routine: Option<String>,
}
impl PgServerError {
pub fn from_fields(fields: Vec<(u8, String)>) -> Self {
let mut err = PgServerError::default();
for (code, value) in fields {
match code {
b'S' => err.severity = value,
b'V' => err.severity_v = Some(value),
b'C' => err.code = value,
b'M' => err.message = value,
b'D' => err.detail = Some(value),
b'H' => err.hint = Some(value),
b'P' => err.position = value.parse().ok(),
b'p' => err.internal_position = value.parse().ok(),
b'q' => err.internal_query = Some(value),
b'W' => err.where_ = Some(value),
b's' => err.schema = Some(value),
b't' => err.table = Some(value),
b'c' => err.column = Some(value),
b'd' => err.data_type = Some(value),
b'n' => err.constraint = Some(value),
b'F' => err.file = Some(value),
b'L' => err.line = value.parse().ok(),
b'R' => err.routine = Some(value),
_ => {} }
}
err
}
pub fn from_error_body(body: &ErrorResponseBody) -> Result<Self, std::io::Error> {
let mut fields = Vec::new();
let mut iter = body.fields();
while let Some(field) = iter.next()? {
let value = std::str::from_utf8(field.value_bytes())
.unwrap_or("")
.to_string();
fields.push((field.type_(), value));
}
Ok(Self::from_fields(fields))
}
pub fn from_notice_body(body: &NoticeResponseBody) -> Result<Self, std::io::Error> {
let mut fields = Vec::new();
let mut iter = body.fields();
while let Some(field) = iter.next()? {
let value = std::str::from_utf8(field.value_bytes())
.unwrap_or("")
.to_string();
fields.push((field.type_(), value));
}
Ok(Self::from_fields(fields))
}
pub fn code(&self) -> &str {
&self.code
}
pub fn is_class(&self, class: &str) -> bool {
self.code.starts_with(class)
}
pub fn is_integrity_constraint_violation(&self) -> bool {
self.is_class("23")
}
pub fn is_unique_violation(&self) -> bool {
self.code == "23505"
}
pub fn is_foreign_key_violation(&self) -> bool {
self.code == "23503"
}
pub fn is_not_null_violation(&self) -> bool {
self.code == "23502"
}
pub fn is_check_violation(&self) -> bool {
self.code == "23514"
}
pub fn is_exclusion_violation(&self) -> bool {
self.code == "23P01"
}
pub fn is_syntax_error(&self) -> bool {
self.is_class("42")
}
pub fn is_insufficient_privilege(&self) -> bool {
self.code == "42501"
}
pub fn is_undefined_table(&self) -> bool {
self.code == "42P01"
}
pub fn is_undefined_column(&self) -> bool {
self.code == "42703"
}
pub fn is_serialization_failure(&self) -> bool {
self.code == "40001"
}
pub fn is_deadlock_detected(&self) -> bool {
self.code == "40P01"
}
pub fn is_connection_exception(&self) -> bool {
self.is_class("08")
}
pub fn is_connection_does_not_exist(&self) -> bool {
self.code == "08003"
}
pub fn is_connection_failure(&self) -> bool {
self.code == "08006"
}
pub fn is_sqlclient_unable_to_establish_sqlconnection(&self) -> bool {
self.code == "08001"
}
pub fn is_query_canceled(&self) -> bool {
self.code == "57014"
}
pub fn is_admin_shutdown(&self) -> bool {
self.code == "57P01"
}
pub fn is_crash_shutdown(&self) -> bool {
self.code == "57P02"
}
pub fn is_cannot_connect_now(&self) -> bool {
self.code == "57P03"
}
pub fn is_database_dropped(&self) -> bool {
self.code == "57P04"
}
pub fn is_idle_session_timeout(&self) -> bool {
self.code == "57P05"
}
pub fn is_fatal(&self) -> bool {
self.severity == "FATAL" || self.severity == "PANIC"
}
pub fn is_warning_or_less(&self) -> bool {
matches!(
self.severity.as_str(),
"WARNING" | "NOTICE" | "DEBUG" | "INFO" | "LOG"
)
}
pub fn schema(&self) -> Option<&str> {
self.schema.as_deref()
}
pub fn table(&self) -> Option<&str> {
self.table.as_deref()
}
pub fn column(&self) -> Option<&str> {
self.column.as_deref()
}
pub fn constraint(&self) -> Option<&str> {
self.constraint.as_deref()
}
pub fn detail(&self) -> Option<&str> {
self.detail.as_deref()
}
pub fn hint(&self) -> Option<&str> {
self.hint.as_deref()
}
pub fn position(&self) -> Option<u32> {
self.position
}
}
impl std::fmt::Display for PgServerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}: {} (SQLSTATE {})",
self.severity, self.message, self.code
)?;
if let Some(detail) = &self.detail {
write!(f, "\nDETAIL: {}", detail)?;
}
if let Some(hint) = &self.hint {
write!(f, "\nHINT: {}", hint)?;
}
if let Some(position) = self.position {
write!(f, "\nPOSITION: {}", position)?;
}
Ok(())
}
}
impl std::error::Error for PgServerError {}
#[cfg(test)]
mod tests {
#![allow(clippy::field_reassign_with_default)]
use super::*;
#[test]
fn test_from_fields_all_fields() {
let fields = vec![
(b'S', "ERROR".to_string()),
(b'V', "ERROR".to_string()),
(b'C', "23505".to_string()),
(
b'M',
"duplicate key value violates unique constraint".to_string(),
),
(b'D', "Key (id)=(1) already exists.".to_string()),
(b'H', "Try a different value.".to_string()),
(b'P', "42".to_string()),
(b'p', "10".to_string()),
(b'q', "SELECT ...".to_string()),
(b'W', "PL/pgSQL function ...".to_string()),
(b's', "public".to_string()),
(b't', "users".to_string()),
(b'c', "id".to_string()),
(b'd', "integer".to_string()),
(b'n', "users_pkey".to_string()),
(b'F', "nbtinsert.c".to_string()),
(b'L', "532".to_string()),
(b'R', "_bt_check_unique".to_string()),
];
let err = PgServerError::from_fields(fields);
assert_eq!(err.severity, "ERROR");
assert_eq!(err.severity_v.as_deref(), Some("ERROR"));
assert_eq!(err.code, "23505");
assert_eq!(
err.message,
"duplicate key value violates unique constraint"
);
assert_eq!(err.detail.as_deref(), Some("Key (id)=(1) already exists."));
assert_eq!(err.hint.as_deref(), Some("Try a different value."));
assert_eq!(err.position, Some(42));
assert_eq!(err.internal_position, Some(10));
assert_eq!(err.internal_query.as_deref(), Some("SELECT ..."));
assert_eq!(err.where_.as_deref(), Some("PL/pgSQL function ..."));
assert_eq!(err.schema.as_deref(), Some("public"));
assert_eq!(err.table.as_deref(), Some("users"));
assert_eq!(err.column.as_deref(), Some("id"));
assert_eq!(err.data_type.as_deref(), Some("integer"));
assert_eq!(err.constraint.as_deref(), Some("users_pkey"));
assert_eq!(err.file.as_deref(), Some("nbtinsert.c"));
assert_eq!(err.line, Some(532));
assert_eq!(err.routine.as_deref(), Some("_bt_check_unique"));
}
#[test]
fn test_from_fields_minimal() {
let fields = vec![
(b'S', "ERROR".to_string()),
(b'C', "42601".to_string()),
(b'M', "syntax error".to_string()),
];
let err = PgServerError::from_fields(fields);
assert_eq!(err.severity, "ERROR");
assert_eq!(err.code, "42601");
assert_eq!(err.message, "syntax error");
assert!(err.detail.is_none());
assert!(err.hint.is_none());
assert!(err.position.is_none());
}
#[test]
fn test_from_fields_unknown_field_ignored() {
let fields = vec![
(b'S', "ERROR".to_string()),
(b'C', "42601".to_string()),
(b'M', "syntax error".to_string()),
(b'X', "unknown field".to_string()), ];
let err = PgServerError::from_fields(fields);
assert_eq!(err.message, "syntax error");
}
#[test]
fn test_sqlstate_classification() {
let mut err = PgServerError::default();
err.code = "23505".to_string();
assert!(err.is_unique_violation());
assert!(err.is_integrity_constraint_violation());
assert!(!err.is_syntax_error());
err.code = "42601".to_string();
assert!(err.is_syntax_error());
assert!(!err.is_integrity_constraint_violation());
err.code = "23503".to_string();
assert!(err.is_foreign_key_violation());
err.code = "23502".to_string();
assert!(err.is_not_null_violation());
err.code = "23514".to_string();
assert!(err.is_check_violation());
err.code = "23P01".to_string();
assert!(err.is_exclusion_violation());
err.code = "42501".to_string();
assert!(err.is_insufficient_privilege());
err.code = "42P01".to_string();
assert!(err.is_undefined_table());
err.code = "42703".to_string();
assert!(err.is_undefined_column());
err.code = "40001".to_string();
assert!(err.is_serialization_failure());
err.code = "40P01".to_string();
assert!(err.is_deadlock_detected());
err.code = "08006".to_string();
assert!(err.is_connection_exception());
assert!(err.is_connection_failure());
err.code = "57014".to_string();
assert!(err.is_query_canceled());
}
#[test]
fn test_severity_checks() {
let mut err = PgServerError::default();
err.severity = "FATAL".to_string();
assert!(err.is_fatal());
assert!(!err.is_warning_or_less());
err.severity = "PANIC".to_string();
assert!(err.is_fatal());
err.severity = "ERROR".to_string();
assert!(!err.is_fatal());
assert!(!err.is_warning_or_less());
err.severity = "WARNING".to_string();
assert!(err.is_warning_or_less());
err.severity = "DEBUG".to_string();
assert!(err.is_warning_or_less());
err.severity = "INFO".to_string();
assert!(err.is_warning_or_less());
err.severity = "LOG".to_string();
assert!(err.is_warning_or_less());
}
#[test]
fn test_display_format() {
let err = PgServerError::from_fields(vec![
(b'S', "ERROR".to_string()),
(b'C', "23505".to_string()),
(b'M', "duplicate key".to_string()),
(b'D', "Key (id)=(1) already exists.".to_string()),
(b'H', "Try a different value.".to_string()),
(b'P', "42".to_string()),
]);
let display = err.to_string();
assert!(display.contains("ERROR: duplicate key (SQLSTATE 23505)"));
assert!(display.contains("DETAIL: Key (id)=(1) already exists."));
assert!(display.contains("HINT: Try a different value."));
assert!(display.contains("POSITION: 42"));
}
#[test]
fn test_display_format_minimal() {
let err = PgServerError::from_fields(vec![
(b'S', "ERROR".to_string()),
(b'C', "42601".to_string()),
(b'M', "syntax error".to_string()),
]);
let display = err.to_string();
assert_eq!(display, "ERROR: syntax error (SQLSTATE 42601)");
}
#[test]
fn test_convenience_accessors() {
let err = PgServerError::from_fields(vec![
(b'S', "ERROR".to_string()),
(b'C', "23505".to_string()),
(b'M', "duplicate key".to_string()),
(b'D', "some detail".to_string()),
(b'H', "some hint".to_string()),
(b's', "public".to_string()),
(b't', "users".to_string()),
(b'c', "id".to_string()),
(b'n', "users_pkey".to_string()),
(b'P', "42".to_string()),
]);
assert_eq!(err.schema(), Some("public"));
assert_eq!(err.table(), Some("users"));
assert_eq!(err.column(), Some("id"));
assert_eq!(err.constraint(), Some("users_pkey"));
assert_eq!(err.detail(), Some("some detail"));
assert_eq!(err.hint(), Some("some hint"));
assert_eq!(err.position(), Some(42));
}
#[test]
fn test_connection_related_codes() {
let mut err = PgServerError::default();
err.code = "08003".to_string();
assert!(err.is_connection_does_not_exist());
err.code = "08001".to_string();
assert!(err.is_sqlclient_unable_to_establish_sqlconnection());
err.code = "57P01".to_string();
assert!(err.is_admin_shutdown());
err.code = "57P02".to_string();
assert!(err.is_crash_shutdown());
err.code = "57P03".to_string();
assert!(err.is_cannot_connect_now());
err.code = "57P04".to_string();
assert!(err.is_database_dropped());
err.code = "57P05".to_string();
assert!(err.is_idle_session_timeout());
}
}