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