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// INTERNAL ERROR TAXONOMY — ARCHITECTURAL CONTRACT
10// ============================================================================
11//
12// This file defines the canonical runtime error classification system for
13// icydb-core. It is the single source of truth for:
14//
15//   • ErrorClass   (semantic domain)
16//   • ErrorOrigin  (subsystem boundary)
17//   • Structured detail payloads
18//   • Canonical constructor entry points
19//
20// -----------------------------------------------------------------------------
21// DESIGN INTENT
22// -----------------------------------------------------------------------------
23//
24// 1. InternalError is a *taxonomy carrier*, not a formatting utility.
25//
26//    - ErrorClass represents semantic meaning (corruption, invariant_violation,
27//      unsupported, etc).
28//    - ErrorOrigin represents the subsystem boundary (store, index, query,
29//      executor, serialize, interface, etc).
30//    - The (class, origin) pair must remain stable and intentional.
31//
32// 2. Call sites MUST prefer canonical constructors.
33//
34//    Do NOT construct errors manually via:
35//        InternalError::new(class, origin, ...)
36//    unless you are defining a new canonical helper here.
37//
38//    If a pattern appears more than once, centralize it here.
39//
40// 3. Constructors in this file must represent real architectural boundaries.
41//
42//    Add a new helper ONLY if it:
43//
44//      • Encodes a cross-cutting invariant,
45//      • Represents a subsystem boundary,
46//      • Or prevents taxonomy drift across call sites.
47//
48//    Do NOT add feature-specific helpers.
49//    Do NOT add one-off formatting helpers.
50//    Do NOT turn this file into a generic message factory.
51//
52// 4. ErrorDetail must align with ErrorOrigin.
53//
54//    If detail is present, it MUST correspond to the origin.
55//    Do not attach mismatched detail variants.
56//
57// 5. Plan-layer errors are NOT runtime failures.
58//
59//    PlanError and CursorPlanError must be translated into
60//    executor/query invariants via the canonical mapping functions.
61//    Do not leak plan-layer error types across execution boundaries.
62//
63// 6. Preserve taxonomy stability.
64//
65//    Do NOT:
66//      • Merge error classes.
67//      • Reclassify corruption as internal.
68//      • Downgrade invariant violations.
69//      • Introduce ambiguous class/origin combinations.
70//
71//    Any change to ErrorClass or ErrorOrigin is an architectural change
72//    and must be reviewed accordingly.
73//
74// -----------------------------------------------------------------------------
75// NON-GOALS
76// -----------------------------------------------------------------------------
77//
78// This is NOT:
79//
80//   • A public API contract.
81//   • A generic error abstraction layer.
82//   • A feature-specific message builder.
83//   • A dumping ground for temporary error conversions.
84//
85// -----------------------------------------------------------------------------
86// MAINTENANCE GUIDELINES
87// -----------------------------------------------------------------------------
88//
89// When modifying this file:
90//
91//   1. Ensure classification semantics remain consistent.
92//   2. Avoid constructor proliferation.
93//   3. Prefer narrow, origin-specific helpers over ad-hoc new(...).
94//   4. Keep formatting minimal and standardized.
95//   5. Keep this file boring and stable.
96//
97// If this file grows rapidly, something is wrong at the call sites.
98//
99// ============================================================================
100
101///
102/// InternalError
103///
104/// Structured runtime error with a stable internal classification.
105/// Not a stable API; intended for internal use and may change without notice.
106///
107
108#[derive(Debug, ThisError)]
109#[error("{message}")]
110pub struct InternalError {
111    pub class: ErrorClass,
112    pub origin: ErrorOrigin,
113    pub message: String,
114
115    /// Optional structured error detail.
116    /// The variant (if present) must correspond to `origin`.
117    pub detail: Option<ErrorDetail>,
118}
119
120impl InternalError {
121    /// Construct an InternalError with optional origin-specific detail.
122    /// This constructor provides default StoreError details for certain
123    /// (class, origin) combinations but does not guarantee a detail payload.
124    pub fn new(class: ErrorClass, origin: ErrorOrigin, message: impl Into<String>) -> Self {
125        let message = message.into();
126
127        let detail = match (class, origin) {
128            (ErrorClass::Corruption, ErrorOrigin::Store) => {
129                Some(ErrorDetail::Store(StoreError::Corrupt {
130                    message: message.clone(),
131                }))
132            }
133            (ErrorClass::InvariantViolation, ErrorOrigin::Store) => {
134                Some(ErrorDetail::Store(StoreError::InvariantViolation {
135                    message: message.clone(),
136                }))
137            }
138            _ => None,
139        };
140
141        Self {
142            class,
143            origin,
144            message,
145            detail,
146        }
147    }
148
149    /// Construct an error while preserving an explicit class/origin taxonomy pair.
150    pub(crate) fn classified(
151        class: ErrorClass,
152        origin: ErrorOrigin,
153        message: impl Into<String>,
154    ) -> Self {
155        Self::new(class, origin, message)
156    }
157
158    /// Rebuild this error with a new message while preserving class/origin taxonomy.
159    pub(crate) fn with_message(self, message: impl Into<String>) -> Self {
160        Self::classified(self.class, self.origin, message)
161    }
162
163    /// Construct a query-origin invariant violation.
164    pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
165        Self::new(
166            ErrorClass::InvariantViolation,
167            ErrorOrigin::Query,
168            message.into(),
169        )
170    }
171
172    /// Build the canonical executor-invariant message prefix.
173    #[must_use]
174    pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
175        format!("executor invariant violated: {}", reason.into())
176    }
177
178    /// Build the canonical invalid-logical-plan message prefix.
179    #[must_use]
180    pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
181        format!("invalid logical plan: {}", reason.into())
182    }
183
184    /// Construct a query-origin invariant with the canonical executor prefix.
185    pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
186        Self::query_invariant(Self::executor_invariant_message(reason))
187    }
188
189    /// Construct a query-origin invariant with the canonical invalid-plan prefix.
190    pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
191        Self::query_invariant(Self::invalid_logical_plan_message(reason))
192    }
193
194    /// Construct an index-origin invariant violation.
195    pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
196        Self::new(
197            ErrorClass::InvariantViolation,
198            ErrorOrigin::Index,
199            message.into(),
200        )
201    }
202
203    /// Construct an executor-origin invariant violation.
204    pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
205        Self::new(
206            ErrorClass::InvariantViolation,
207            ErrorOrigin::Executor,
208            message.into(),
209        )
210    }
211
212    /// Construct a store-origin invariant violation.
213    pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
214        Self::new(
215            ErrorClass::InvariantViolation,
216            ErrorOrigin::Store,
217            message.into(),
218        )
219    }
220
221    /// Construct a store-origin internal error.
222    pub(crate) fn store_internal(message: impl Into<String>) -> Self {
223        Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
224    }
225
226    /// Construct an executor-origin internal error.
227    pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
228        Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
229    }
230
231    /// Construct an index-origin internal error.
232    pub(crate) fn index_internal(message: impl Into<String>) -> Self {
233        Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
234    }
235
236    /// Construct a query-origin internal error.
237    #[cfg(test)]
238    pub(crate) fn query_internal(message: impl Into<String>) -> Self {
239        Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
240    }
241
242    /// Construct a serialize-origin internal error.
243    pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
244        Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
245    }
246
247    /// Construct a store-origin corruption error.
248    pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
249        Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
250    }
251
252    /// Construct an index-origin corruption error.
253    pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
254        Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
255    }
256
257    /// Construct a serialize-origin corruption error.
258    pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
259        Self::new(
260            ErrorClass::Corruption,
261            ErrorOrigin::Serialize,
262            message.into(),
263        )
264    }
265
266    /// Construct a store-origin unsupported error.
267    pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
268        Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
269    }
270
271    /// Construct an index-origin unsupported error.
272    pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
273        Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
274    }
275
276    /// Construct an executor-origin unsupported error.
277    pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
278        Self::new(
279            ErrorClass::Unsupported,
280            ErrorOrigin::Executor,
281            message.into(),
282        )
283    }
284
285    /// Construct a serialize-origin unsupported error.
286    pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
287        Self::new(
288            ErrorClass::Unsupported,
289            ErrorOrigin::Serialize,
290            message.into(),
291        )
292    }
293
294    pub fn store_not_found(key: impl Into<String>) -> Self {
295        let key = key.into();
296
297        Self {
298            class: ErrorClass::NotFound,
299            origin: ErrorOrigin::Store,
300            message: format!("data key not found: {key}"),
301            detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
302        }
303    }
304
305    /// Construct a standardized unsupported-entity-path error.
306    pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
307        let path = path.into();
308
309        Self::new(
310            ErrorClass::Unsupported,
311            ErrorOrigin::Store,
312            format!("unsupported entity path: '{path}'"),
313        )
314    }
315
316    #[must_use]
317    pub const fn is_not_found(&self) -> bool {
318        matches!(
319            self.detail,
320            Some(ErrorDetail::Store(StoreError::NotFound { .. }))
321        )
322    }
323
324    #[must_use]
325    pub fn display_with_class(&self) -> String {
326        format!("{}:{}: {}", self.origin, self.class, self.message)
327    }
328
329    /// Construct an index-plan corruption error with a canonical prefix.
330    pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
331        let message = message.into();
332        Self::new(
333            ErrorClass::Corruption,
334            origin,
335            format!("corruption detected ({origin}): {message}"),
336        )
337    }
338
339    /// Construct an index-plan corruption error for index-origin failures.
340    pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
341        Self::index_plan_corruption(ErrorOrigin::Index, message)
342    }
343
344    /// Construct an index-plan corruption error for store-origin failures.
345    pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
346        Self::index_plan_corruption(ErrorOrigin::Store, message)
347    }
348
349    /// Construct an index-plan corruption error for serialize-origin failures.
350    pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
351        Self::index_plan_corruption(ErrorOrigin::Serialize, message)
352    }
353
354    /// Construct an index uniqueness violation conflict error.
355    pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
356        Self::new(
357            ErrorClass::Conflict,
358            ErrorOrigin::Index,
359            format!(
360                "index constraint violation: {path} ({})",
361                index_fields.join(", ")
362            ),
363        )
364    }
365
366    /// Map plan-surface cursor failures into executor-boundary invariants.
367    pub(crate) fn from_cursor_plan_error(err: PlanError) -> Self {
368        let message = match &err {
369            PlanError::Cursor(inner) => match inner.as_ref() {
370                CursorPlanError::ContinuationCursorBoundaryArityMismatch { expected: 1, found } => {
371                    Self::executor_invariant_message(format!(
372                        "pk-ordered continuation boundary must contain exactly 1 slot, found {found}"
373                    ))
374                }
375                CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
376                    value: None, ..
377                } => Self::executor_invariant_message("pk cursor slot must be present"),
378                CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
379                    value: Some(_),
380                    ..
381                } => Self::executor_invariant_message("pk cursor slot type mismatch"),
382                _ => err.to_string(),
383            },
384            _ => err.to_string(),
385        };
386
387        Self::query_invariant(message)
388    }
389
390    /// Map shared plan-validation failures into executor-boundary invariants.
391    pub(crate) fn from_executor_plan_error(err: PlanError) -> Self {
392        Self::query_invariant(err.to_string())
393    }
394}
395
396///
397/// ErrorDetail
398///
399/// Structured, origin-specific error detail carried by [`InternalError`].
400/// This enum is intentionally extensible.
401///
402
403#[derive(Debug, ThisError)]
404pub enum ErrorDetail {
405    #[error("{0}")]
406    Store(StoreError),
407    #[error("{0}")]
408    ViewPatch(crate::patch::MergePatchError),
409    // Future-proofing:
410    // #[error("{0}")]
411    // Index(IndexError),
412    //
413    // #[error("{0}")]
414    // Query(QueryErrorDetail),
415    //
416    // #[error("{0}")]
417    // Executor(ExecutorErrorDetail),
418}
419
420impl From<MergePatchError> for InternalError {
421    fn from(err: MergePatchError) -> Self {
422        Self {
423            class: ErrorClass::Unsupported,
424            origin: ErrorOrigin::Interface,
425            message: err.to_string(),
426            detail: Some(ErrorDetail::ViewPatch(err)),
427        }
428    }
429}
430
431///
432/// StoreError
433///
434/// Store-specific structured error detail.
435/// Never returned directly; always wrapped in [`ErrorDetail::Store`].
436///
437
438#[derive(Debug, ThisError)]
439pub enum StoreError {
440    #[error("key not found: {key}")]
441    NotFound { key: String },
442
443    #[error("store corruption: {message}")]
444    Corrupt { message: String },
445
446    #[error("store invariant violation: {message}")]
447    InvariantViolation { message: String },
448}
449
450///
451/// ErrorClass
452/// Internal error taxonomy for runtime classification.
453/// Not a stable API; may change without notice.
454///
455
456#[derive(Clone, Copy, Debug, Eq, PartialEq)]
457pub enum ErrorClass {
458    Corruption,
459    NotFound,
460    Internal,
461    Conflict,
462    Unsupported,
463    InvariantViolation,
464}
465
466impl fmt::Display for ErrorClass {
467    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468        let label = match self {
469            Self::Corruption => "corruption",
470            Self::NotFound => "not_found",
471            Self::Internal => "internal",
472            Self::Conflict => "conflict",
473            Self::Unsupported => "unsupported",
474            Self::InvariantViolation => "invariant_violation",
475        };
476        write!(f, "{label}")
477    }
478}
479
480///
481/// ErrorOrigin
482/// Internal origin taxonomy for runtime classification.
483/// Not a stable API; may change without notice.
484///
485
486#[derive(Clone, Copy, Debug, Eq, PartialEq)]
487pub enum ErrorOrigin {
488    Serialize,
489    Store,
490    Index,
491    Query,
492    Response,
493    Executor,
494    Interface,
495}
496
497impl fmt::Display for ErrorOrigin {
498    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499        let label = match self {
500            Self::Serialize => "serialize",
501            Self::Store => "store",
502            Self::Index => "index",
503            Self::Query => "query",
504            Self::Response => "response",
505            Self::Executor => "executor",
506            Self::Interface => "interface",
507        };
508        write!(f, "{label}")
509    }
510}
511
512///
513/// TESTS
514///
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use crate::db::query::plan::{CursorPlanError, PlanError};
520
521    #[test]
522    fn index_plan_index_corruption_uses_index_origin() {
523        let err = InternalError::index_plan_index_corruption("broken key payload");
524        assert_eq!(err.class, ErrorClass::Corruption);
525        assert_eq!(err.origin, ErrorOrigin::Index);
526        assert_eq!(
527            err.message,
528            "corruption detected (index): broken key payload"
529        );
530    }
531
532    #[test]
533    fn index_plan_store_corruption_uses_store_origin() {
534        let err = InternalError::index_plan_store_corruption("row/key mismatch");
535        assert_eq!(err.class, ErrorClass::Corruption);
536        assert_eq!(err.origin, ErrorOrigin::Store);
537        assert_eq!(err.message, "corruption detected (store): row/key mismatch");
538    }
539
540    #[test]
541    fn index_plan_serialize_corruption_uses_serialize_origin() {
542        let err = InternalError::index_plan_serialize_corruption("decode failed");
543        assert_eq!(err.class, ErrorClass::Corruption);
544        assert_eq!(err.origin, ErrorOrigin::Serialize);
545        assert_eq!(
546            err.message,
547            "corruption detected (serialize): decode failed"
548        );
549    }
550
551    #[test]
552    fn query_executor_invariant_uses_invariant_violation_class() {
553        let err = InternalError::query_executor_invariant("route contract mismatch");
554        assert_eq!(err.class, ErrorClass::InvariantViolation);
555        assert_eq!(err.origin, ErrorOrigin::Query);
556    }
557
558    #[test]
559    fn executor_plan_error_mapping_stays_invariant_violation() {
560        let plan_err = PlanError::from(CursorPlanError::InvalidContinuationCursorPayload {
561            reason: "bad token".to_string(),
562        });
563        let err = InternalError::from_executor_plan_error(plan_err);
564        assert_eq!(err.class, ErrorClass::InvariantViolation);
565        assert_eq!(err.origin, ErrorOrigin::Query);
566    }
567}