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}