use core::fmt;
use reliar_core::{Classify, FailureKind, MessageId};
#[derive(Debug)]
#[non_exhaustive]
pub enum PostgresStoreError {
SchemaResolution {
configured: String,
observed: String,
},
NotMigrated {
schema: String,
},
Database {
source: sqlx::Error,
},
Decode {
id: MessageId,
detail: String,
},
UnknownMetadataVersion {
id: MessageId,
version: i32,
},
DuplicateMessage {
id: MessageId,
},
InvalidSchema {
schema: String,
},
}
impl fmt::Display for PostgresStoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SchemaResolution {
configured,
observed,
} => write!(
f,
"outbox did not resolve to schema \"{configured}\" (observed search_path: \
\"{observed}\"); set search_path so \"{configured}\" comes first, e.g. \
ALTER ROLE <role> SET search_path = {configured}, public"
),
Self::NotMigrated { schema } => write!(
f,
"relation \"{schema}.outbox\" does not exist; call \
reliar_store_postgres::migrate(&pool, ..) before constructing the store"
),
Self::Database { source } => write!(f, "database error: {source}"),
Self::Decode { id, detail } => write!(f, "row {id} could not be decoded: {detail}"),
Self::UnknownMetadataVersion { id, version } => {
write!(f, "row {id} carries unknown metadata_version {version}")
}
Self::DuplicateMessage { id } => {
write!(f, "message id {id} already exists in the outbox")
}
Self::InvalidSchema { schema } => write!(
f,
"{schema:?} is not a valid PostgreSQL identifier (expected \
[A-Za-z_][A-Za-z0-9_$]*, at most 63 bytes)"
),
}
}
}
impl std::error::Error for PostgresStoreError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Database { source } => Some(source),
_ => None,
}
}
}
impl Classify for PostgresStoreError {
fn kind(&self) -> FailureKind {
match self {
Self::SchemaResolution { .. }
| Self::NotMigrated { .. }
| Self::InvalidSchema { .. }
| Self::DuplicateMessage { .. }
| Self::Decode { .. }
| Self::UnknownMetadataVersion { .. } => FailureKind::Permanent,
Self::Database { source } => classify_sqlstate(source),
}
}
}
pub(crate) fn classify_sqlstate(err: &sqlx::Error) -> FailureKind {
let sqlx::Error::Database(db) = err else {
return FailureKind::Transient;
};
let Some(code) = db.code() else {
return FailureKind::Transient;
};
match code.as_ref().get(..2) {
Some("08" | "40" | "53" | "55") => FailureKind::Transient,
Some("57") if code.as_ref() == "57014" => FailureKind::Transient,
Some("22" | "23" | "42") => FailureKind::Permanent,
_ => {
tracing::warn!(sqlstate = %code, "unrecognised SQLSTATE; classifying transient");
FailureKind::Transient
}
}
}
impl From<sqlx::Error> for PostgresStoreError {
fn from(source: sqlx::Error) -> Self {
Self::Database { source }
}
}
pub(crate) fn map_operational_error(schema: &str, err: sqlx::Error) -> PostgresStoreError {
if is_undefined_table(&err) {
return PostgresStoreError::NotMigrated {
schema: schema.to_owned(),
};
}
PostgresStoreError::Database { source: err }
}
#[derive(Debug)]
#[non_exhaustive]
pub enum EnqueueError<E> {
Serialize {
source: E,
},
Duplicate {
id: MessageId,
},
Database {
source: sqlx::Error,
},
}
impl<E: fmt::Display> fmt::Display for EnqueueError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Serialize { source } => {
write!(f, "failed to serialize the envelope body: {source}")
}
Self::Duplicate { id } => write!(f, "message id {id} already exists in the outbox"),
Self::Database { source } => write!(f, "database error: {source}"),
}
}
}
impl<E: std::error::Error + 'static> std::error::Error for EnqueueError<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Serialize { source } => Some(source),
Self::Database { source } => Some(source),
Self::Duplicate { .. } => None,
}
}
}
impl<E: std::error::Error + Send + Sync + 'static> Classify for EnqueueError<E> {
fn kind(&self) -> FailureKind {
match self {
Self::Serialize { .. } | Self::Duplicate { .. } => FailureKind::Permanent,
Self::Database { source } => classify_sqlstate(source),
}
}
}
pub(crate) fn map_enqueue_error<E>(id: MessageId, err: sqlx::Error) -> EnqueueError<E> {
if is_constraint_violation(&err, "pk_outbox") {
return EnqueueError::Duplicate { id };
}
EnqueueError::Database { source: err }
}
pub(crate) fn is_constraint_violation(err: &sqlx::Error, constraint: &str) -> bool {
match err {
sqlx::Error::Database(db) => db.constraint() == Some(constraint),
_ => false,
}
}
pub(crate) fn is_undefined_table(err: &sqlx::Error) -> bool {
match err {
sqlx::Error::Database(db) => db.code().as_deref() == Some("42P01"),
_ => false,
}
}
pub(crate) fn is_valid_schema_name(schema: &str) -> bool {
let mut chars = schema.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
schema.len() <= 63 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
}