Skip to main content

icydb_core/
error.rs

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