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 an error while preserving an explicit class/origin taxonomy pair.
57    pub(crate) fn classified(
58        class: ErrorClass,
59        origin: ErrorOrigin,
60        message: impl Into<String>,
61    ) -> Self {
62        Self::new(class, origin, message)
63    }
64
65    /// Rebuild this error with a new message while preserving class/origin taxonomy.
66    pub(crate) fn with_message(self, message: impl Into<String>) -> Self {
67        Self::classified(self.class, self.origin, message)
68    }
69
70    /// Construct a query-origin invariant violation.
71    pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
72        Self::new(
73            ErrorClass::InvariantViolation,
74            ErrorOrigin::Query,
75            message.into(),
76        )
77    }
78
79    /// Build the canonical executor-invariant message prefix.
80    #[must_use]
81    pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
82        format!("executor invariant violated: {}", reason.into())
83    }
84
85    /// Build the canonical invalid-logical-plan message prefix.
86    #[must_use]
87    pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
88        format!("invalid logical plan: {}", reason.into())
89    }
90
91    /// Construct a query-origin invariant with the canonical executor prefix.
92    pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
93        Self::query_invariant(Self::executor_invariant_message(reason))
94    }
95
96    /// Construct a query-origin invariant with the canonical invalid-plan prefix.
97    pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
98        Self::query_invariant(Self::invalid_logical_plan_message(reason))
99    }
100
101    /// Construct an index-origin invariant violation.
102    pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
103        Self::new(
104            ErrorClass::InvariantViolation,
105            ErrorOrigin::Index,
106            message.into(),
107        )
108    }
109
110    /// Construct an executor-origin invariant violation.
111    pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
112        Self::new(
113            ErrorClass::InvariantViolation,
114            ErrorOrigin::Executor,
115            message.into(),
116        )
117    }
118
119    /// Construct a store-origin invariant violation.
120    pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
121        Self::new(
122            ErrorClass::InvariantViolation,
123            ErrorOrigin::Store,
124            message.into(),
125        )
126    }
127
128    /// Construct a store-origin internal error.
129    pub(crate) fn store_internal(message: impl Into<String>) -> Self {
130        Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
131    }
132
133    /// Construct an executor-origin internal error.
134    pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
135        Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
136    }
137
138    /// Construct an index-origin internal error.
139    pub(crate) fn index_internal(message: impl Into<String>) -> Self {
140        Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
141    }
142
143    /// Construct a query-origin internal error.
144    #[cfg(test)]
145    pub(crate) fn query_internal(message: impl Into<String>) -> Self {
146        Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
147    }
148
149    /// Construct a serialize-origin internal error.
150    pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
151        Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
152    }
153
154    /// Construct a store-origin corruption error.
155    pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
156        Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
157    }
158
159    /// Construct an index-origin corruption error.
160    pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
161        Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
162    }
163
164    /// Construct a serialize-origin corruption error.
165    pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
166        Self::new(
167            ErrorClass::Corruption,
168            ErrorOrigin::Serialize,
169            message.into(),
170        )
171    }
172
173    /// Construct a store-origin unsupported error.
174    pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
175        Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
176    }
177
178    /// Construct an index-origin unsupported error.
179    pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
180        Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
181    }
182
183    /// Construct an executor-origin unsupported error.
184    pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
185        Self::new(
186            ErrorClass::Unsupported,
187            ErrorOrigin::Executor,
188            message.into(),
189        )
190    }
191
192    /// Construct a serialize-origin unsupported error.
193    pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
194        Self::new(
195            ErrorClass::Unsupported,
196            ErrorOrigin::Serialize,
197            message.into(),
198        )
199    }
200
201    pub fn store_not_found(key: impl Into<String>) -> Self {
202        let key = key.into();
203
204        Self {
205            class: ErrorClass::NotFound,
206            origin: ErrorOrigin::Store,
207            message: format!("data key not found: {key}"),
208            detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
209        }
210    }
211
212    /// Construct a standardized unsupported-entity-path error.
213    pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
214        let path = path.into();
215
216        Self::new(
217            ErrorClass::Unsupported,
218            ErrorOrigin::Store,
219            format!("unsupported entity path: '{path}'"),
220        )
221    }
222
223    #[must_use]
224    pub const fn is_not_found(&self) -> bool {
225        matches!(
226            self.detail,
227            Some(ErrorDetail::Store(StoreError::NotFound { .. }))
228        )
229    }
230
231    #[must_use]
232    pub fn display_with_class(&self) -> String {
233        format!("{}:{}: {}", self.origin, self.class, self.message)
234    }
235
236    /// Construct an index-plan corruption error with a canonical prefix.
237    pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
238        let message = message.into();
239        Self::new(
240            ErrorClass::Corruption,
241            origin,
242            format!("corruption detected ({origin}): {message}"),
243        )
244    }
245
246    /// Construct an index uniqueness violation conflict error.
247    pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
248        Self::new(
249            ErrorClass::Conflict,
250            ErrorOrigin::Index,
251            format!(
252                "index constraint violation: {path} ({})",
253                index_fields.join(", ")
254            ),
255        )
256    }
257
258    /// Map plan-surface cursor failures into executor-boundary invariants.
259    pub(crate) fn from_cursor_plan_error(err: PlanError) -> Self {
260        let message = match &err {
261            PlanError::Cursor(inner) => match inner.as_ref() {
262                CursorPlanError::ContinuationCursorBoundaryArityMismatch { expected: 1, found } => {
263                    Self::executor_invariant_message(format!(
264                        "pk-ordered continuation boundary must contain exactly 1 slot, found {found}"
265                    ))
266                }
267                CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
268                    value: None, ..
269                } => Self::executor_invariant_message("pk cursor slot must be present"),
270                CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
271                    value: Some(_),
272                    ..
273                } => Self::executor_invariant_message("pk cursor slot type mismatch"),
274                _ => err.to_string(),
275            },
276            _ => err.to_string(),
277        };
278
279        Self::query_invariant(message)
280    }
281
282    /// Map shared plan-validation failures into executor-boundary invariants.
283    pub(crate) fn from_executor_plan_error(err: PlanError) -> Self {
284        Self::query_invariant(err.to_string())
285    }
286}
287
288///
289/// ErrorDetail
290///
291/// Structured, origin-specific error detail carried by [`InternalError`].
292/// This enum is intentionally extensible.
293///
294
295#[derive(Debug, ThisError)]
296pub enum ErrorDetail {
297    #[error("{0}")]
298    Store(StoreError),
299    #[error("{0}")]
300    ViewPatch(crate::patch::MergePatchError),
301    // Future-proofing:
302    // #[error("{0}")]
303    // Index(IndexError),
304    //
305    // #[error("{0}")]
306    // Query(QueryErrorDetail),
307    //
308    // #[error("{0}")]
309    // Executor(ExecutorErrorDetail),
310}
311
312impl From<MergePatchError> for InternalError {
313    fn from(err: MergePatchError) -> Self {
314        Self {
315            class: ErrorClass::Unsupported,
316            origin: ErrorOrigin::Interface,
317            message: err.to_string(),
318            detail: Some(ErrorDetail::ViewPatch(err)),
319        }
320    }
321}
322
323///
324/// StoreError
325///
326/// Store-specific structured error detail.
327/// Never returned directly; always wrapped in [`ErrorDetail::Store`].
328///
329
330#[derive(Debug, ThisError)]
331pub enum StoreError {
332    #[error("key not found: {key}")]
333    NotFound { key: String },
334
335    #[error("store corruption: {message}")]
336    Corrupt { message: String },
337
338    #[error("store invariant violation: {message}")]
339    InvariantViolation { message: String },
340}
341
342///
343/// ErrorClass
344/// Internal error taxonomy for runtime classification.
345/// Not a stable API; may change without notice.
346///
347
348#[derive(Clone, Copy, Debug, Eq, PartialEq)]
349pub enum ErrorClass {
350    Corruption,
351    NotFound,
352    Internal,
353    Conflict,
354    Unsupported,
355    InvariantViolation,
356}
357
358impl fmt::Display for ErrorClass {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        let label = match self {
361            Self::Corruption => "corruption",
362            Self::NotFound => "not_found",
363            Self::Internal => "internal",
364            Self::Conflict => "conflict",
365            Self::Unsupported => "unsupported",
366            Self::InvariantViolation => "invariant_violation",
367        };
368        write!(f, "{label}")
369    }
370}
371
372///
373/// ErrorOrigin
374/// Internal origin taxonomy for runtime classification.
375/// Not a stable API; may change without notice.
376///
377
378#[derive(Clone, Copy, Debug, Eq, PartialEq)]
379pub enum ErrorOrigin {
380    Serialize,
381    Store,
382    Index,
383    Query,
384    Response,
385    Executor,
386    Interface,
387}
388
389impl fmt::Display for ErrorOrigin {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        let label = match self {
392            Self::Serialize => "serialize",
393            Self::Store => "store",
394            Self::Index => "index",
395            Self::Query => "query",
396            Self::Response => "response",
397            Self::Executor => "executor",
398            Self::Interface => "interface",
399        };
400        write!(f, "{label}")
401    }
402}