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    /// Construct a query-origin invariant violation.
166    pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
167        Self::new(
168            ErrorClass::InvariantViolation,
169            ErrorOrigin::Query,
170            message.into(),
171        )
172    }
173
174    /// Build the canonical executor-invariant message prefix.
175    #[must_use]
176    pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
177        format!("executor invariant violated: {}", reason.into())
178    }
179
180    /// Build the canonical invalid-logical-plan message prefix.
181    #[must_use]
182    pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
183        format!("invalid logical plan: {}", reason.into())
184    }
185
186    /// Construct a query-origin invariant with the canonical executor prefix.
187    pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
188        Self::query_invariant(Self::executor_invariant_message(reason))
189    }
190
191    /// Construct a query-origin invariant with the canonical invalid-plan prefix.
192    pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
193        Self::query_invariant(Self::invalid_logical_plan_message(reason))
194    }
195
196    /// Construct an index-origin invariant violation.
197    pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
198        Self::new(
199            ErrorClass::InvariantViolation,
200            ErrorOrigin::Index,
201            message.into(),
202        )
203    }
204
205    /// Construct an executor-origin invariant violation.
206    pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
207        Self::new(
208            ErrorClass::InvariantViolation,
209            ErrorOrigin::Executor,
210            message.into(),
211        )
212    }
213
214    /// Construct a store-origin invariant violation.
215    pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
216        Self::new(
217            ErrorClass::InvariantViolation,
218            ErrorOrigin::Store,
219            message.into(),
220        )
221    }
222
223    /// Construct a store-origin internal error.
224    pub(crate) fn store_internal(message: impl Into<String>) -> Self {
225        Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
226    }
227
228    /// Construct an executor-origin internal error.
229    pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
230        Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
231    }
232
233    /// Construct an index-origin internal error.
234    pub(crate) fn index_internal(message: impl Into<String>) -> Self {
235        Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
236    }
237
238    /// Construct a query-origin internal error.
239    #[cfg(test)]
240    pub(crate) fn query_internal(message: impl Into<String>) -> Self {
241        Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
242    }
243
244    /// Construct a serialize-origin internal error.
245    pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
246        Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
247    }
248
249    /// Construct a store-origin corruption error.
250    pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
251        Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
252    }
253
254    /// Construct an index-origin corruption error.
255    pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
256        Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
257    }
258
259    /// Construct a serialize-origin corruption error.
260    pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
261        Self::new(
262            ErrorClass::Corruption,
263            ErrorOrigin::Serialize,
264            message.into(),
265        )
266    }
267
268    /// Construct a store-origin unsupported error.
269    pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
270        Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
271    }
272
273    /// Construct an index-origin unsupported error.
274    pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
275        Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
276    }
277
278    /// Construct an executor-origin unsupported error.
279    pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
280        Self::new(
281            ErrorClass::Unsupported,
282            ErrorOrigin::Executor,
283            message.into(),
284        )
285    }
286
287    /// Construct a serialize-origin unsupported error.
288    pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
289        Self::new(
290            ErrorClass::Unsupported,
291            ErrorOrigin::Serialize,
292            message.into(),
293        )
294    }
295
296    pub fn store_not_found(key: impl Into<String>) -> Self {
297        let key = key.into();
298
299        Self {
300            class: ErrorClass::NotFound,
301            origin: ErrorOrigin::Store,
302            message: format!("data key not found: {key}"),
303            detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
304        }
305    }
306
307    /// Construct a standardized unsupported-entity-path error.
308    pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
309        let path = path.into();
310
311        Self::new(
312            ErrorClass::Unsupported,
313            ErrorOrigin::Store,
314            format!("unsupported entity path: '{path}'"),
315        )
316    }
317
318    #[must_use]
319    pub const fn is_not_found(&self) -> bool {
320        matches!(
321            self.detail,
322            Some(ErrorDetail::Store(StoreError::NotFound { .. }))
323        )
324    }
325
326    #[must_use]
327    pub fn display_with_class(&self) -> String {
328        format!("{}:{}: {}", self.origin, self.class, self.message)
329    }
330
331    /// Construct an index-plan corruption error with a canonical prefix.
332    pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
333        let message = message.into();
334        Self::new(
335            ErrorClass::Corruption,
336            origin,
337            format!("corruption detected ({origin}): {message}"),
338        )
339    }
340
341    /// Construct an index-plan corruption error for index-origin failures.
342    pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
343        Self::index_plan_corruption(ErrorOrigin::Index, message)
344    }
345
346    /// Construct an index-plan corruption error for store-origin failures.
347    pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
348        Self::index_plan_corruption(ErrorOrigin::Store, message)
349    }
350
351    /// Construct an index-plan corruption error for serialize-origin failures.
352    pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
353        Self::index_plan_corruption(ErrorOrigin::Serialize, message)
354    }
355
356    /// Construct an index uniqueness violation conflict error.
357    pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
358        Self::new(
359            ErrorClass::Conflict,
360            ErrorOrigin::Index,
361            format!(
362                "index constraint violation: {path} ({})",
363                index_fields.join(", ")
364            ),
365        )
366    }
367
368    /// Map plan-surface cursor failures into executor-boundary invariants.
369    pub(crate) fn from_cursor_plan_error(err: CursorPlanError) -> Self {
370        let message = match &err {
371            CursorPlanError::ContinuationCursorBoundaryArityMismatch { expected: 1, found } => {
372                Self::executor_invariant_message(format!(
373                    "pk-ordered continuation boundary must contain exactly 1 slot, found {found}"
374                ))
375            }
376            CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch { value: None, .. } => {
377                Self::executor_invariant_message("pk cursor slot must be present")
378            }
379            CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
380                value: Some(_), ..
381            } => Self::executor_invariant_message("pk cursor slot type mismatch"),
382            _ => err.to_string(),
383        };
384
385        Self::query_invariant(message)
386    }
387
388    /// Map grouped plan failures into query-boundary invariants.
389    #[cfg(test)]
390    pub(crate) fn from_group_plan_error(err: PlanError) -> Self {
391        let message = match &err {
392            PlanError::Group(inner) => format!("invalid logical plan: {inner}"),
393            _ => err.to_string(),
394        };
395
396        Self::query_invariant(message)
397    }
398
399    /// Map shared access-validation failures into executor-boundary invariants.
400    pub(crate) fn from_executor_access_plan_error(err: AccessPlanError) -> Self {
401        Self::query_invariant(err.to_string())
402    }
403
404    /// Map plan-shape policy variants into executor-boundary invariants without
405    /// string-based conversion paths.
406    #[cfg(test)]
407    pub(crate) fn plan_invariant_violation(err: PolicyPlanError) -> Self {
408        let reason = match err {
409            PolicyPlanError::EmptyOrderSpec => {
410                "order specification must include at least one field"
411            }
412            PolicyPlanError::DeletePlanWithPagination => "delete plans must not include pagination",
413            PolicyPlanError::LoadPlanWithDeleteLimit => "load plans must not carry delete limits",
414            PolicyPlanError::DeleteLimitRequiresOrder => "delete limit requires explicit ordering",
415            PolicyPlanError::UnorderedPagination => "pagination requires explicit ordering",
416        };
417
418        Self::query_executor_invariant(reason)
419    }
420}
421
422///
423/// ErrorDetail
424///
425/// Structured, origin-specific error detail carried by [`InternalError`].
426/// This enum is intentionally extensible.
427///
428
429#[derive(Debug, ThisError)]
430pub enum ErrorDetail {
431    #[error("{0}")]
432    Store(StoreError),
433    #[error("{0}")]
434    ViewPatch(crate::patch::MergePatchError),
435    // Future-proofing:
436    // #[error("{0}")]
437    // Index(IndexError),
438    //
439    // #[error("{0}")]
440    // Query(QueryErrorDetail),
441    //
442    // #[error("{0}")]
443    // Executor(ExecutorErrorDetail),
444}
445
446impl From<MergePatchError> for InternalError {
447    fn from(err: MergePatchError) -> Self {
448        Self {
449            class: ErrorClass::Unsupported,
450            origin: ErrorOrigin::Interface,
451            message: err.to_string(),
452            detail: Some(ErrorDetail::ViewPatch(err)),
453        }
454    }
455}
456
457///
458/// StoreError
459///
460/// Store-specific structured error detail.
461/// Never returned directly; always wrapped in [`ErrorDetail::Store`].
462///
463
464#[derive(Debug, ThisError)]
465pub enum StoreError {
466    #[error("key not found: {key}")]
467    NotFound { key: String },
468
469    #[error("store corruption: {message}")]
470    Corrupt { message: String },
471
472    #[error("store invariant violation: {message}")]
473    InvariantViolation { message: String },
474}
475
476///
477/// ErrorClass
478/// Internal error taxonomy for runtime classification.
479/// Not a stable API; may change without notice.
480///
481
482#[derive(Clone, Copy, Debug, Eq, PartialEq)]
483pub enum ErrorClass {
484    Corruption,
485    NotFound,
486    Internal,
487    Conflict,
488    Unsupported,
489    InvariantViolation,
490}
491
492impl fmt::Display for ErrorClass {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        let label = match self {
495            Self::Corruption => "corruption",
496            Self::NotFound => "not_found",
497            Self::Internal => "internal",
498            Self::Conflict => "conflict",
499            Self::Unsupported => "unsupported",
500            Self::InvariantViolation => "invariant_violation",
501        };
502        write!(f, "{label}")
503    }
504}
505
506///
507/// ErrorOrigin
508/// Internal origin taxonomy for runtime classification.
509/// Not a stable API; may change without notice.
510///
511
512#[derive(Clone, Copy, Debug, Eq, PartialEq)]
513pub enum ErrorOrigin {
514    Serialize,
515    Store,
516    Index,
517    Query,
518    Response,
519    Executor,
520    Interface,
521}
522
523impl fmt::Display for ErrorOrigin {
524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525        let label = match self {
526            Self::Serialize => "serialize",
527            Self::Store => "store",
528            Self::Index => "index",
529            Self::Query => "query",
530            Self::Response => "response",
531            Self::Executor => "executor",
532            Self::Interface => "interface",
533        };
534        write!(f, "{label}")
535    }
536}
537
538///
539/// TESTS
540///
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use crate::db::query::plan::validate::GroupPlanError;
546
547    #[test]
548    fn index_plan_index_corruption_uses_index_origin() {
549        let err = InternalError::index_plan_index_corruption("broken key payload");
550        assert_eq!(err.class, ErrorClass::Corruption);
551        assert_eq!(err.origin, ErrorOrigin::Index);
552        assert_eq!(
553            err.message,
554            "corruption detected (index): broken key payload"
555        );
556    }
557
558    #[test]
559    fn index_plan_store_corruption_uses_store_origin() {
560        let err = InternalError::index_plan_store_corruption("row/key mismatch");
561        assert_eq!(err.class, ErrorClass::Corruption);
562        assert_eq!(err.origin, ErrorOrigin::Store);
563        assert_eq!(err.message, "corruption detected (store): row/key mismatch");
564    }
565
566    #[test]
567    fn index_plan_serialize_corruption_uses_serialize_origin() {
568        let err = InternalError::index_plan_serialize_corruption("decode failed");
569        assert_eq!(err.class, ErrorClass::Corruption);
570        assert_eq!(err.origin, ErrorOrigin::Serialize);
571        assert_eq!(
572            err.message,
573            "corruption detected (serialize): decode failed"
574        );
575    }
576
577    #[test]
578    fn query_executor_invariant_uses_invariant_violation_class() {
579        let err = InternalError::query_executor_invariant("route contract mismatch");
580        assert_eq!(err.class, ErrorClass::InvariantViolation);
581        assert_eq!(err.origin, ErrorOrigin::Query);
582    }
583
584    #[test]
585    fn executor_access_plan_error_mapping_stays_invariant_violation() {
586        let err = InternalError::from_executor_access_plan_error(AccessPlanError::IndexPrefixEmpty);
587        assert_eq!(err.class, ErrorClass::InvariantViolation);
588        assert_eq!(err.origin, ErrorOrigin::Query);
589    }
590
591    #[test]
592    fn plan_policy_error_mapping_uses_executor_invariant_prefix() {
593        let err =
594            InternalError::plan_invariant_violation(PolicyPlanError::DeleteLimitRequiresOrder);
595        assert_eq!(err.class, ErrorClass::InvariantViolation);
596        assert_eq!(err.origin, ErrorOrigin::Query);
597        assert_eq!(
598            err.message,
599            "executor invariant violated: delete limit requires explicit ordering",
600        );
601    }
602
603    #[test]
604    fn group_plan_error_mapping_uses_invalid_logical_plan_prefix() {
605        let err = InternalError::from_group_plan_error(PlanError::from(
606            GroupPlanError::UnknownGroupField {
607                field: "tenant".to_string(),
608            },
609        ));
610
611        assert_eq!(err.class, ErrorClass::InvariantViolation);
612        assert_eq!(err.origin, ErrorOrigin::Query);
613        assert_eq!(
614            err.message,
615            "invalid logical plan: unknown group field 'tenant'",
616        );
617    }
618}