Skip to main content

icydb_core/
error.rs

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