Skip to main content

siderite_orm/
error.rs

1//! ORM error hierarchy. HTTP mapping lives in `siderite-core`.
2
3use crate::capabilities::{BackendKind, Feature};
4use crate::signals::SignalError;
5use thiserror::Error;
6
7/// A feature was requested that the backend does not support.
8#[derive(Debug, Clone, PartialEq, Eq, Error)]
9#[non_exhaustive]
10pub enum BackendCapabilityError {
11    /// `select_for_update` on a backend without row locking.
12    #[error("row locking is not supported by {backend:?}")]
13    RowLockingUnsupported {
14        /// Backend that rejected the request.
15        backend: BackendKind,
16    },
17    /// Any other unsupported feature.
18    #[error("{feature:?} is not supported by {backend:?}")]
19    Unsupported {
20        /// Backend that rejected the request.
21        backend: BackendKind,
22        /// The unsupported feature.
23        feature: Feature,
24    },
25}
26
27impl BackendCapabilityError {
28    /// Build the most specific error variant for `feature`.
29    pub fn from_feature(backend: BackendKind, feature: Feature) -> Self {
30        match feature {
31            Feature::RowLocking => Self::RowLockingUnsupported { backend },
32            feature => Self::Unsupported { backend, feature },
33        }
34    }
35}
36
37/// The query plan is invalid independent of any backend.
38#[derive(Debug, Clone, PartialEq, Eq, Error)]
39#[non_exhaustive]
40pub enum QueryError {
41    /// `get()` matched no rows.
42    #[error("no rows matched the query")]
43    DoesNotExist,
44    /// `get()` matched more than one row.
45    #[error("expected exactly one row, found {0}")]
46    MultipleObjectsReturned(u64),
47    /// Structural problem in the plan.
48    #[error("invalid query plan: {0}")]
49    InvalidPlan(String),
50    /// A statement ran on a transaction that was already committed or rolled back.
51    #[error("the transaction is already closed")]
52    TransactionClosed,
53    /// Cancelled or failed scope cleanup made this transaction unusable.
54    #[error("the transaction was aborted; its writes cannot be committed")]
55    TransactionAborted,
56    /// Another statement or child scope owns this transaction connection.
57    #[error("another statement or child scope owns this transaction")]
58    TransactionBusy,
59    /// A model or relation was used in a way its metadata does not allow.
60    #[error("{0}")]
61    Model(String),
62    /// A decoded value had an unexpected type.
63    #[error("cannot decode column `{column}`: {reason}")]
64    Decode {
65        /// Column name.
66        column: String,
67        /// Reason.
68        reason: String,
69    },
70}
71
72/// A pagination cursor was rejected.
73///
74/// Tokens come from clients, so they are validated, never trusted. Each
75/// variant has a stable [`code`](Self::code) for API clients; the messages
76/// say nothing about which query or column was involved.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
78#[non_exhaustive]
79pub enum CursorError {
80    /// Not base64url, over 1 KiB, truncated, or of the wrong direction.
81    #[error("the cursor is malformed")]
82    Malformed,
83    /// The token layout version is not one this server reads.
84    #[error("the cursor has an unknown version")]
85    UnknownVersion,
86    /// The token was issued for a different query.
87    #[error("the cursor belongs to another query")]
88    ForeignQuery,
89    /// The values do not fit the ordering (count or types).
90    #[error("the cursor does not fit the ordering")]
91    Mismatch,
92}
93
94impl CursorError {
95    /// The stable error code (`cursor.E001` to `cursor.E004`).
96    pub const fn code(self) -> &'static str {
97        match self {
98            Self::Malformed => "cursor.E001",
99            Self::UnknownVersion => "cursor.E002",
100            Self::ForeignQuery => "cursor.E003",
101            Self::Mismatch => "cursor.E004",
102        }
103    }
104}
105
106/// Driver / connection level failure.
107///
108/// Messages come from the driver and may contain SQL fragments; they are
109/// logged, never returned to HTTP clients (see `ApiError: From<OrmError>`).
110#[derive(Debug, Error)]
111#[non_exhaustive]
112pub enum BackendError {
113    /// Could not connect or acquire a connection.
114    #[error("database connection error: {0}")]
115    Connection(String),
116    /// The database rejected or failed a statement.
117    #[error("database error: {0}")]
118    Database(String),
119    /// A constraint (unique, foreign key, check) was violated.
120    #[error("constraint violation: {0}")]
121    Constraint(String),
122}
123
124/// Top-level ORM error.
125#[derive(Debug, Error)]
126#[non_exhaustive]
127pub enum OrmError {
128    /// Query-construction or result error.
129    #[error(transparent)]
130    Query(#[from] QueryError),
131    /// Backend failure.
132    #[error(transparent)]
133    Backend(#[from] BackendError),
134    /// Capability mismatch.
135    #[error(transparent)]
136    Capability(#[from] BackendCapabilityError),
137    /// A pagination cursor from the client was rejected.
138    #[error(transparent)]
139    Cursor(#[from] CursorError),
140    /// A signal receiver failed. A failing `pre_*` receiver aborted the
141    /// operation; a failing `post_*` receiver ran after the statement.
142    /// Maps to HTTP 500 (`ApiError: From<OrmError>` falls through to internal).
143    #[error(transparent)]
144    Signal(#[from] SignalError),
145    /// A database alias is not registered in [`Databases`](crate::Databases).
146    ///
147    /// A configuration problem (a router named an alias that was never
148    /// registered, or there is no `"default"`), so it maps to HTTP 500.
149    #[error("no database is registered under the alias `{0}`")]
150    UnknownDatabase(String),
151}