Skip to main content

icydb_core/
error.rs

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