Skip to main content

icydb_core/
error.rs

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