Skip to main content

icydb_core/
error.rs

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