Skip to main content

icydb_core/
error.rs

1use crate::{
2    db::query::plan::{CursorPlanError, PlanError},
3    patch::MergePatchError,
4};
5use std::fmt;
6use thiserror::Error as ThisError;
7
8///
9/// InternalError
10///
11/// Structured runtime error with a stable internal classification.
12/// Not a stable API; intended for internal use and may change without notice.
13///
14
15#[derive(Debug, ThisError)]
16#[error("{message}")]
17pub struct InternalError {
18    pub class: ErrorClass,
19    pub origin: ErrorOrigin,
20    pub message: String,
21
22    /// Optional structured error detail.
23    /// The variant (if present) must correspond to `origin`.
24    pub detail: Option<ErrorDetail>,
25}
26
27impl InternalError {
28    /// Construct an InternalError with optional origin-specific detail.
29    /// This constructor provides default StoreError details for certain
30    /// (class, origin) combinations but does not guarantee a detail payload.
31    pub fn new(class: ErrorClass, origin: ErrorOrigin, message: impl Into<String>) -> Self {
32        let message = message.into();
33
34        let detail = match (class, origin) {
35            (ErrorClass::Corruption, ErrorOrigin::Store) => {
36                Some(ErrorDetail::Store(StoreError::Corrupt {
37                    message: message.clone(),
38                }))
39            }
40            (ErrorClass::InvariantViolation, ErrorOrigin::Store) => {
41                Some(ErrorDetail::Store(StoreError::InvariantViolation {
42                    message: message.clone(),
43                }))
44            }
45            _ => None,
46        };
47
48        Self {
49            class,
50            origin,
51            message,
52            detail,
53        }
54    }
55
56    /// Construct a query-origin invariant violation.
57    pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
58        Self::new(
59            ErrorClass::InvariantViolation,
60            ErrorOrigin::Query,
61            message.into(),
62        )
63    }
64
65    /// Construct an index-origin invariant violation.
66    pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
67        Self::new(
68            ErrorClass::InvariantViolation,
69            ErrorOrigin::Index,
70            message.into(),
71        )
72    }
73
74    /// Construct an executor-origin invariant violation.
75    pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
76        Self::new(
77            ErrorClass::InvariantViolation,
78            ErrorOrigin::Executor,
79            message.into(),
80        )
81    }
82
83    /// Construct a store-origin invariant violation.
84    pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
85        Self::new(
86            ErrorClass::InvariantViolation,
87            ErrorOrigin::Store,
88            message.into(),
89        )
90    }
91
92    /// Construct a corruption error for a specific origin.
93    pub(crate) fn corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
94        Self::new(ErrorClass::Corruption, origin, message.into())
95    }
96
97    /// Construct a store-origin internal error.
98    pub(crate) fn store_internal(message: impl Into<String>) -> Self {
99        Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
100    }
101
102    /// Construct an executor-origin internal error.
103    pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
104        Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
105    }
106
107    /// Construct an index-origin internal error.
108    pub(crate) fn index_internal(message: impl Into<String>) -> Self {
109        Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
110    }
111
112    /// Construct a serialize-origin internal error.
113    pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
114        Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
115    }
116
117    /// Construct a store-origin corruption error.
118    pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
119        Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
120    }
121
122    /// Construct an index-origin corruption error.
123    pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
124        Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
125    }
126
127    /// Construct a serialize-origin corruption error.
128    pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
129        Self::new(
130            ErrorClass::Corruption,
131            ErrorOrigin::Serialize,
132            message.into(),
133        )
134    }
135
136    /// Construct a store-origin unsupported error.
137    pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
138        Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
139    }
140
141    /// Construct an index-origin unsupported error.
142    pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
143        Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
144    }
145
146    /// Construct an executor-origin unsupported error.
147    pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
148        Self::new(
149            ErrorClass::Unsupported,
150            ErrorOrigin::Executor,
151            message.into(),
152        )
153    }
154
155    /// Construct a serialize-origin unsupported error.
156    pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
157        Self::new(
158            ErrorClass::Unsupported,
159            ErrorOrigin::Serialize,
160            message.into(),
161        )
162    }
163
164    pub fn store_not_found(key: impl Into<String>) -> Self {
165        let key = key.into();
166
167        Self {
168            class: ErrorClass::NotFound,
169            origin: ErrorOrigin::Store,
170            message: format!("data key not found: {key}"),
171            detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
172        }
173    }
174
175    /// Construct a standardized unsupported-entity-path error.
176    pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
177        let path = path.into();
178
179        Self::new(
180            ErrorClass::Unsupported,
181            ErrorOrigin::Store,
182            format!("unsupported entity path: '{path}'"),
183        )
184    }
185
186    #[must_use]
187    pub const fn is_not_found(&self) -> bool {
188        matches!(
189            self.detail,
190            Some(ErrorDetail::Store(StoreError::NotFound { .. }))
191        )
192    }
193
194    #[must_use]
195    pub fn display_with_class(&self) -> String {
196        format!("{}:{}: {}", self.origin, self.class, self.message)
197    }
198
199    /// Construct an index-plan corruption error with a canonical prefix.
200    pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
201        let message = message.into();
202        Self::new(
203            ErrorClass::Corruption,
204            origin,
205            format!("corruption detected ({origin}): {message}"),
206        )
207    }
208
209    /// Construct an index uniqueness violation conflict error.
210    pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
211        Self::new(
212            ErrorClass::Conflict,
213            ErrorOrigin::Index,
214            format!(
215                "index constraint violation: {path} ({})",
216                index_fields.join(", ")
217            ),
218        )
219    }
220
221    /// Map plan-surface cursor failures into executor-boundary invariants.
222    pub(crate) fn from_cursor_plan_error(err: PlanError) -> Self {
223        let message = match &err {
224            PlanError::Cursor(inner) => match inner.as_ref() {
225                CursorPlanError::ContinuationCursorBoundaryArityMismatch { expected: 1, found } => {
226                    format!(
227                        "executor invariant violated: pk-ordered continuation boundary must contain exactly 1 slot, found {found}"
228                    )
229                }
230                CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
231                    value: None, ..
232                } => "executor invariant violated: pk cursor slot must be present".to_string(),
233                CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
234                    value: Some(_),
235                    ..
236                } => "executor invariant violated: pk cursor slot type mismatch".to_string(),
237                _ => err.to_string(),
238            },
239            _ => err.to_string(),
240        };
241
242        Self::query_invariant(message)
243    }
244
245    /// Map shared plan-validation failures into executor-boundary invariants.
246    pub(crate) fn from_executor_plan_error(err: PlanError) -> Self {
247        Self::query_invariant(err.to_string())
248    }
249}
250
251///
252/// ErrorDetail
253///
254/// Structured, origin-specific error detail carried by [`InternalError`].
255/// This enum is intentionally extensible.
256///
257
258#[derive(Debug, ThisError)]
259pub enum ErrorDetail {
260    #[error("{0}")]
261    Store(StoreError),
262    #[error("{0}")]
263    ViewPatch(crate::patch::MergePatchError),
264    // Future-proofing:
265    // #[error("{0}")]
266    // Index(IndexError),
267    //
268    // #[error("{0}")]
269    // Query(QueryErrorDetail),
270    //
271    // #[error("{0}")]
272    // Executor(ExecutorErrorDetail),
273}
274
275impl From<MergePatchError> for InternalError {
276    fn from(err: MergePatchError) -> Self {
277        Self {
278            class: ErrorClass::Unsupported,
279            origin: ErrorOrigin::Interface,
280            message: err.to_string(),
281            detail: Some(ErrorDetail::ViewPatch(err)),
282        }
283    }
284}
285
286///
287/// StoreError
288///
289/// Store-specific structured error detail.
290/// Never returned directly; always wrapped in [`ErrorDetail::Store`].
291///
292
293#[derive(Debug, ThisError)]
294pub enum StoreError {
295    #[error("key not found: {key}")]
296    NotFound { key: String },
297
298    #[error("store corruption: {message}")]
299    Corrupt { message: String },
300
301    #[error("store invariant violation: {message}")]
302    InvariantViolation { message: String },
303}
304
305///
306/// ErrorClass
307/// Internal error taxonomy for runtime classification.
308/// Not a stable API; may change without notice.
309///
310
311#[derive(Clone, Copy, Debug, Eq, PartialEq)]
312pub enum ErrorClass {
313    Corruption,
314    NotFound,
315    Internal,
316    Conflict,
317    Unsupported,
318    InvariantViolation,
319}
320
321impl fmt::Display for ErrorClass {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        let label = match self {
324            Self::Corruption => "corruption",
325            Self::NotFound => "not_found",
326            Self::Internal => "internal",
327            Self::Conflict => "conflict",
328            Self::Unsupported => "unsupported",
329            Self::InvariantViolation => "invariant_violation",
330        };
331        write!(f, "{label}")
332    }
333}
334
335///
336/// ErrorOrigin
337/// Internal origin taxonomy for runtime classification.
338/// Not a stable API; may change without notice.
339///
340
341#[derive(Clone, Copy, Debug, Eq, PartialEq)]
342pub enum ErrorOrigin {
343    Serialize,
344    Store,
345    Index,
346    Query,
347    Response,
348    Executor,
349    Interface,
350}
351
352impl fmt::Display for ErrorOrigin {
353    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354        let label = match self {
355            Self::Serialize => "serialize",
356            Self::Store => "store",
357            Self::Index => "index",
358            Self::Query => "query",
359            Self::Response => "response",
360            Self::Executor => "executor",
361            Self::Interface => "interface",
362        };
363        write!(f, "{label}")
364    }
365}