Skip to main content

icydb_core/error/
mod.rs

1//! Module: error
2//!
3//! Responsibility: module-local ownership and contracts for error.
4//! Does not own: cross-module orchestration outside this module.
5//! Boundary: exposes this module API while keeping implementation details internal.
6
7#[cfg(test)]
8mod tests;
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(crate) class: ErrorClass,
117    pub(crate) origin: ErrorOrigin,
118    pub(crate) message: String,
119
120    /// Optional structured error detail.
121    /// The variant (if present) must correspond to `origin`.
122    pub(crate) 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    /// Return the internal error class taxonomy.
155    #[must_use]
156    pub const fn class(&self) -> ErrorClass {
157        self.class
158    }
159
160    /// Return the internal error origin taxonomy.
161    #[must_use]
162    pub const fn origin(&self) -> ErrorOrigin {
163        self.origin
164    }
165
166    /// Return the rendered internal error message.
167    #[must_use]
168    pub fn message(&self) -> &str {
169        &self.message
170    }
171
172    /// Return the optional structured detail payload.
173    #[must_use]
174    pub const fn detail(&self) -> Option<&ErrorDetail> {
175        self.detail.as_ref()
176    }
177
178    /// Consume and return the rendered internal error message.
179    #[must_use]
180    pub fn into_message(self) -> String {
181        self.message
182    }
183
184    /// Construct an error while preserving an explicit class/origin taxonomy pair.
185    pub(crate) fn classified(
186        class: ErrorClass,
187        origin: ErrorOrigin,
188        message: impl Into<String>,
189    ) -> Self {
190        Self::new(class, origin, message)
191    }
192
193    /// Rebuild this error with a new message while preserving class/origin taxonomy.
194    pub(crate) fn with_message(self, message: impl Into<String>) -> Self {
195        Self::classified(self.class, self.origin, message)
196    }
197
198    /// Rebuild this error with a new origin while preserving class/message.
199    ///
200    /// Origin-scoped detail payloads are intentionally dropped when re-origining.
201    pub(crate) fn with_origin(self, origin: ErrorOrigin) -> Self {
202        Self::classified(self.class, origin, self.message)
203    }
204
205    /// Construct an index-origin invariant violation.
206    pub(crate) fn index_invariant(message: impl Into<String>) -> Self {
207        Self::new(
208            ErrorClass::InvariantViolation,
209            ErrorOrigin::Index,
210            message.into(),
211        )
212    }
213
214    /// Construct a planner-origin invariant violation with the canonical
215    /// executor-boundary invariant prefix preserved in the message payload.
216    pub(crate) fn planner_executor_invariant(reason: impl Into<String>) -> Self {
217        Self::new(
218            ErrorClass::InvariantViolation,
219            ErrorOrigin::Planner,
220            Self::executor_invariant_message(reason),
221        )
222    }
223
224    /// Construct a query-origin invariant violation with the canonical
225    /// executor-boundary invariant prefix preserved in the message payload.
226    pub(crate) fn query_executor_invariant(reason: impl Into<String>) -> Self {
227        Self::new(
228            ErrorClass::InvariantViolation,
229            ErrorOrigin::Query,
230            Self::executor_invariant_message(reason),
231        )
232    }
233
234    /// Construct an executor-origin invariant violation.
235    pub(crate) fn executor_invariant(message: impl Into<String>) -> Self {
236        Self::new(
237            ErrorClass::InvariantViolation,
238            ErrorOrigin::Executor,
239            message.into(),
240        )
241    }
242
243    /// Construct an executor-origin internal error.
244    pub(crate) fn executor_internal(message: impl Into<String>) -> Self {
245        Self::new(ErrorClass::Internal, ErrorOrigin::Executor, message.into())
246    }
247
248    /// Construct an executor-origin unsupported error.
249    pub(crate) fn executor_unsupported(message: impl Into<String>) -> Self {
250        Self::new(
251            ErrorClass::Unsupported,
252            ErrorOrigin::Executor,
253            message.into(),
254        )
255    }
256
257    /// Build the canonical executor-invariant message prefix.
258    #[must_use]
259    pub(crate) fn executor_invariant_message(reason: impl Into<String>) -> String {
260        format!("executor invariant violated: {}", reason.into())
261    }
262
263    /// Construct a planner-origin invariant violation.
264    pub(crate) fn planner_invariant(message: impl Into<String>) -> Self {
265        Self::new(
266            ErrorClass::InvariantViolation,
267            ErrorOrigin::Planner,
268            message.into(),
269        )
270    }
271
272    /// Build the canonical invalid-logical-plan message prefix.
273    #[must_use]
274    pub(crate) fn invalid_logical_plan_message(reason: impl Into<String>) -> String {
275        format!("invalid logical plan: {}", reason.into())
276    }
277
278    /// Construct a planner-origin invariant with the canonical invalid-plan prefix.
279    pub(crate) fn query_invalid_logical_plan(reason: impl Into<String>) -> Self {
280        Self::planner_invariant(Self::invalid_logical_plan_message(reason))
281    }
282
283    /// Construct a query-origin invariant violation.
284    pub(crate) fn query_invariant(message: impl Into<String>) -> Self {
285        Self::new(
286            ErrorClass::InvariantViolation,
287            ErrorOrigin::Query,
288            message.into(),
289        )
290    }
291
292    /// Construct a cursor-origin invariant violation.
293    pub(crate) fn cursor_invariant(message: impl Into<String>) -> Self {
294        Self::new(
295            ErrorClass::InvariantViolation,
296            ErrorOrigin::Cursor,
297            message.into(),
298        )
299    }
300
301    /// Construct a store-origin invariant violation.
302    pub(crate) fn store_invariant(message: impl Into<String>) -> Self {
303        Self::new(
304            ErrorClass::InvariantViolation,
305            ErrorOrigin::Store,
306            message.into(),
307        )
308    }
309
310    /// Construct a store-origin internal error.
311    pub(crate) fn store_internal(message: impl Into<String>) -> Self {
312        Self::new(ErrorClass::Internal, ErrorOrigin::Store, message.into())
313    }
314
315    /// Construct an index-origin internal error.
316    pub(crate) fn index_internal(message: impl Into<String>) -> Self {
317        Self::new(ErrorClass::Internal, ErrorOrigin::Index, message.into())
318    }
319
320    /// Construct a query-origin internal error.
321    #[cfg(test)]
322    pub(crate) fn query_internal(message: impl Into<String>) -> Self {
323        Self::new(ErrorClass::Internal, ErrorOrigin::Query, message.into())
324    }
325
326    /// Construct a query-origin unsupported error.
327    pub(crate) fn query_unsupported(message: impl Into<String>) -> Self {
328        Self::new(ErrorClass::Unsupported, ErrorOrigin::Query, message.into())
329    }
330
331    /// Construct a serialize-origin internal error.
332    pub(crate) fn serialize_internal(message: impl Into<String>) -> Self {
333        Self::new(ErrorClass::Internal, ErrorOrigin::Serialize, message.into())
334    }
335
336    /// Construct a store-origin corruption error.
337    pub(crate) fn store_corruption(message: impl Into<String>) -> Self {
338        Self::new(ErrorClass::Corruption, ErrorOrigin::Store, message.into())
339    }
340
341    /// Construct an index-origin corruption error.
342    pub(crate) fn index_corruption(message: impl Into<String>) -> Self {
343        Self::new(ErrorClass::Corruption, ErrorOrigin::Index, message.into())
344    }
345
346    /// Construct a serialize-origin corruption error.
347    pub(crate) fn serialize_corruption(message: impl Into<String>) -> Self {
348        Self::new(
349            ErrorClass::Corruption,
350            ErrorOrigin::Serialize,
351            message.into(),
352        )
353    }
354
355    /// Construct an identity-origin corruption error.
356    pub(crate) fn identity_corruption(message: impl Into<String>) -> Self {
357        Self::new(
358            ErrorClass::Corruption,
359            ErrorOrigin::Identity,
360            message.into(),
361        )
362    }
363
364    /// Construct a store-origin unsupported error.
365    pub(crate) fn store_unsupported(message: impl Into<String>) -> Self {
366        Self::new(ErrorClass::Unsupported, ErrorOrigin::Store, message.into())
367    }
368
369    /// Construct an index-origin unsupported error.
370    pub(crate) fn index_unsupported(message: impl Into<String>) -> Self {
371        Self::new(ErrorClass::Unsupported, ErrorOrigin::Index, message.into())
372    }
373
374    /// Construct a serialize-origin unsupported error.
375    pub(crate) fn serialize_unsupported(message: impl Into<String>) -> Self {
376        Self::new(
377            ErrorClass::Unsupported,
378            ErrorOrigin::Serialize,
379            message.into(),
380        )
381    }
382
383    /// Construct a cursor-origin unsupported error.
384    pub(crate) fn cursor_unsupported(message: impl Into<String>) -> Self {
385        Self::new(ErrorClass::Unsupported, ErrorOrigin::Cursor, message.into())
386    }
387
388    /// Construct a serialize-origin incompatible persisted-format error.
389    pub(crate) fn serialize_incompatible_persisted_format(message: impl Into<String>) -> Self {
390        Self::new(
391            ErrorClass::IncompatiblePersistedFormat,
392            ErrorOrigin::Serialize,
393            message.into(),
394        )
395    }
396
397    /// Construct a query-origin unsupported error preserving one SQL parser
398    /// unsupported-feature label in structured error detail.
399    #[cfg(feature = "sql")]
400    pub(crate) fn query_unsupported_sql_feature(feature: &'static str) -> Self {
401        let message = format!(
402            "SQL query is not executable in this release: unsupported SQL feature: {feature}"
403        );
404
405        Self {
406            class: ErrorClass::Unsupported,
407            origin: ErrorOrigin::Query,
408            message,
409            detail: Some(ErrorDetail::Query(
410                QueryErrorDetail::UnsupportedSqlFeature { feature },
411            )),
412        }
413    }
414
415    pub fn store_not_found(key: impl Into<String>) -> Self {
416        let key = key.into();
417
418        Self {
419            class: ErrorClass::NotFound,
420            origin: ErrorOrigin::Store,
421            message: format!("data key not found: {key}"),
422            detail: Some(ErrorDetail::Store(StoreError::NotFound { key })),
423        }
424    }
425
426    /// Construct a standardized unsupported-entity-path error.
427    pub fn unsupported_entity_path(path: impl Into<String>) -> Self {
428        let path = path.into();
429
430        Self::new(
431            ErrorClass::Unsupported,
432            ErrorOrigin::Store,
433            format!("unsupported entity path: '{path}'"),
434        )
435    }
436
437    #[must_use]
438    pub const fn is_not_found(&self) -> bool {
439        matches!(
440            self.detail,
441            Some(ErrorDetail::Store(StoreError::NotFound { .. }))
442        )
443    }
444
445    #[must_use]
446    pub fn display_with_class(&self) -> String {
447        format!("{}:{}: {}", self.origin, self.class, self.message)
448    }
449
450    /// Construct an index-plan corruption error with a canonical prefix.
451    pub(crate) fn index_plan_corruption(origin: ErrorOrigin, message: impl Into<String>) -> Self {
452        let message = message.into();
453        Self::new(
454            ErrorClass::Corruption,
455            origin,
456            format!("corruption detected ({origin}): {message}"),
457        )
458    }
459
460    /// Construct an index-plan corruption error for index-origin failures.
461    pub(crate) fn index_plan_index_corruption(message: impl Into<String>) -> Self {
462        Self::index_plan_corruption(ErrorOrigin::Index, message)
463    }
464
465    /// Construct an index-plan corruption error for store-origin failures.
466    pub(crate) fn index_plan_store_corruption(message: impl Into<String>) -> Self {
467        Self::index_plan_corruption(ErrorOrigin::Store, message)
468    }
469
470    /// Construct an index-plan corruption error for serialize-origin failures.
471    pub(crate) fn index_plan_serialize_corruption(message: impl Into<String>) -> Self {
472        Self::index_plan_corruption(ErrorOrigin::Serialize, message)
473    }
474
475    /// Construct an index-plan invariant violation error with a canonical prefix.
476    pub(crate) fn index_plan_invariant(origin: ErrorOrigin, message: impl Into<String>) -> Self {
477        let message = message.into();
478        Self::new(
479            ErrorClass::InvariantViolation,
480            origin,
481            format!("invariant violation detected ({origin}): {message}"),
482        )
483    }
484
485    /// Construct an index-plan invariant violation error for store-origin failures.
486    pub(crate) fn index_plan_store_invariant(message: impl Into<String>) -> Self {
487        Self::index_plan_invariant(ErrorOrigin::Store, message)
488    }
489
490    /// Construct an index uniqueness violation conflict error.
491    pub(crate) fn index_violation(path: &str, index_fields: &[&str]) -> Self {
492        Self::new(
493            ErrorClass::Conflict,
494            ErrorOrigin::Index,
495            format!(
496                "index constraint violation: {path} ({})",
497                index_fields.join(", ")
498            ),
499        )
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    Query(QueryErrorDetail),
516    // Future-proofing:
517    // #[error("{0}")]
518    // Index(IndexError),
519    //
520    // #[error("{0}")]
521    // Executor(ExecutorErrorDetail),
522}
523
524///
525/// StoreError
526///
527/// Store-specific structured error detail.
528/// Never returned directly; always wrapped in [`ErrorDetail::Store`].
529///
530
531#[derive(Debug, ThisError)]
532pub enum StoreError {
533    #[error("key not found: {key}")]
534    NotFound { key: String },
535
536    #[error("store corruption: {message}")]
537    Corrupt { message: String },
538
539    #[error("store invariant violation: {message}")]
540    InvariantViolation { message: String },
541}
542
543///
544/// QueryErrorDetail
545///
546/// Query-origin structured error detail payload.
547///
548
549#[derive(Debug, ThisError)]
550pub enum QueryErrorDetail {
551    #[error("unsupported SQL feature: {feature}")]
552    UnsupportedSqlFeature { feature: &'static str },
553}
554
555///
556/// ErrorClass
557/// Internal error taxonomy for runtime classification.
558/// Not a stable API; may change without notice.
559///
560
561#[derive(Clone, Copy, Debug, Eq, PartialEq)]
562pub enum ErrorClass {
563    Corruption,
564    IncompatiblePersistedFormat,
565    NotFound,
566    Internal,
567    Conflict,
568    Unsupported,
569    InvariantViolation,
570}
571
572impl fmt::Display for ErrorClass {
573    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
574        let label = match self {
575            Self::Corruption => "corruption",
576            Self::IncompatiblePersistedFormat => "incompatible_persisted_format",
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}