arcature-data 2026.2.0

Arcature high-level data layer: explicit-ownership model/query ergonomics over SeaORM/SQLx, N+1 detection, and migration lint.
Documentation
//! The typed [`DataError`] for the high-level data layer (AGENTS.md §18).
//!
//! Errors are typed and contextual — no raw `String` errors. A database error
//! from SeaORM preserves the upstream [`sea_orm::DbErr`] (which itself carries
//! context — connection, query, constraint, etc.); a raw SQLx error preserves
//! [`sqlx::Error`]. Invalid pagination arguments are a distinct, typed variant
//! so a caller can branch on the failure mode without string matching.

use std::fmt;

use arcature_db::sea_orm;
use arcature_db::sqlx;

/// The error returned by the high-level data layer.
///
/// One database-error variant preserves the upstream SeaORM error (SeaORM's
/// `DbErr` is already contextual — it distinguishes connection, query, record
/// not found, and constraint failures). A separate variant preserves raw
/// SQLx errors for the escape-hatch path. Pagination validation failures are
/// a distinct typed enum so they never collide with a database failure.
#[derive(Debug)]
pub enum DataError {
    /// A SeaORM query, mutation, or transaction returned a database error.
    Database(sea_orm::DbErr),
    /// A raw SQLx operation (the escape hatch) returned a database error.
    Sqlx(sqlx::Error),
    /// A pagination argument was invalid (zero is not a valid page or
    /// per-page size).
    Pagination(PaginationError),
}

/// An invalid pagination argument (AGENTS.md §18: typed, not a string).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaginationError {
    /// The requested page number was zero; pages are 1-based.
    PageMustBePositive,
    /// The requested per-page size was zero; it must be at least 1.
    PerPageMustBePositive,
}

impl DataError {
    /// The stable, machine-readable error identifier (for structured logging
    /// and `arc doctor`-style diagnostics). Stable strings, never the dynamic
    /// message — callers may match on these.
    #[must_use]
    pub fn id(&self) -> &'static str {
        match self {
            Self::Database(_) => "arcature_data.database",
            Self::Sqlx(_) => "arcature_data.sqlx",
            Self::Pagination(PaginationError::PageMustBePositive) => {
                "arcature_data.pagination.page_must_be_positive"
            }
            Self::Pagination(PaginationError::PerPageMustBePositive) => {
                "arcature_data.pagination.per_page_must_be_positive"
            }
        }
    }
}

impl fmt::Display for DataError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Database(error) => write!(formatter, "database error: {error}"),
            Self::Sqlx(error) => write!(formatter, "sqlx error: {error}"),
            Self::Pagination(PaginationError::PageMustBePositive) => {
                write!(formatter, "pagination page must be >= 1 (1-based)")
            }
            Self::Pagination(PaginationError::PerPageMustBePositive) => {
                write!(formatter, "pagination per_page must be >= 1")
            }
        }
    }
}

impl std::error::Error for DataError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Database(error) => Some(error),
            Self::Sqlx(error) => Some(error),
            Self::Pagination(_) => None,
        }
    }
}

impl From<sea_orm::DbErr> for DataError {
    fn from(value: sea_orm::DbErr) -> Self {
        Self::Database(value)
    }
}

impl From<sqlx::Error> for DataError {
    fn from(value: sqlx::Error) -> Self {
        Self::Sqlx(value)
    }
}

impl From<PaginationError> for DataError {
    fn from(value: PaginationError) -> Self {
        Self::Pagination(value)
    }
}