Skip to main content

icydb_core/
error.rs

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