reliar-store-postgres 0.8.0

PostgreSQL provider for the Reliar transactional outbox and inbox: migrations, migrate(), enqueue and the SKIP LOCKED claim.
Documentation
//! SQLSTATE classification shared by every error enum in this crate (ADR 0008): the outbox's
//! [`crate::PostgresOutboxError`]/[`crate::EnqueueError`], the inbox's
//! [`crate::PostgresInboxError`], and [`crate::MigrateError`]. No error type lives here — each
//! side owns its own (`crate::outbox::error`, `crate::inbox::error`, `crate::migrate`), except for
//! the [`FromDatabaseError`] trait [`crate::connection::session::Session::map_err`] is generic
//! over (layout Part II §7.2): the two sides map a `sqlx::Error` differently and must keep doing so, so
//! this is a trait rather than a field on `Session`.

use reliar_core::FailureKind;

/// A crate error type's own mapping from a raw `sqlx::Error` — what
/// [`crate::connection::session::Session::map_err`] calls. Each side keeps its own impl
/// (`crate::outbox::error::map_operational_error`, `crate::inbox::error::map_operational_error`)
/// rather than sharing one body: only the `NotMigrated`/`Database` split is common, and each
/// enum's other variants are never reached from here.
pub(crate) trait FromDatabaseError {
    fn from_database_error(err: sqlx::Error) -> Self;
}

/// Classifies a `sqlx::Error` by its wrapped SQLSTATE **class**, never by message text:
///
/// - **Transient** — `08*` (connection exception), `40*` (transaction rollback: deadlock,
///   serialization failure), `53*` (insufficient resources), `55*` (object in use), `57014`
///   (`query_canceled`, i.e. a `statement_timeout`), and any pool/IO error with no SQLSTATE at all.
/// - **Permanent** — `22*` (data exception), `23*` (integrity constraint violation), `42*`
///   (syntax error or access rule violation — includes `42P01`, mapped to `NotMigrated` before
///   this function ever sees it).
/// - Anything unrecognised classifies **Transient** — an unknown fault is more often weather
///   than logic — but is logged at `warn` with its SQLSTATE so this table can be extended.
pub(crate) fn classify_sqlstate(err: &sqlx::Error) -> FailureKind {
    let sqlx::Error::Database(db) = err else {
        // No SQLSTATE at all: a connection/IO/pool-exhaustion failure, not a data problem.
        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
        }
    }
}

/// `true` for SQLSTATE `42P01` (`undefined_table`) — the relation is missing, i.e. `migrate()`
/// has not run.
pub(crate) fn is_undefined_table(err: &sqlx::Error) -> bool {
    match err {
        sqlx::Error::Database(db) => db.code().as_deref() == Some("42P01"),
        _ => false,
    }
}