Skip to main content

icydb/
error.rs

1use candid::CandidType;
2use icydb_core::{
3    db::{QueryError, QueryExecutionError, ResponseError},
4    error::{ErrorClass as CoreErrorClass, ErrorOrigin as CoreErrorOrigin, InternalError},
5};
6use serde::Deserialize;
7use thiserror::Error as ThisError;
8
9//
10// Error
11//
12
13#[cfg_attr(doc, doc = "Error\n\nPublic error payload.")]
14#[derive(CandidType, Debug, Deserialize, ThisError)]
15#[error("{message}")]
16pub struct Error {
17    kind: ErrorKind,
18    origin: ErrorOrigin,
19    message: String,
20}
21
22impl Error {
23    pub fn new(kind: ErrorKind, origin: ErrorOrigin, message: impl Into<String>) -> Self {
24        Self {
25            kind,
26            origin,
27            message: message.into(),
28        }
29    }
30
31    fn from_response_error(err: ResponseError) -> Self {
32        match err {
33            ResponseError::NotFound { .. } => Self::new(
34                ErrorKind::Query(QueryErrorKind::NotFound),
35                ErrorOrigin::Response,
36                err.to_string(),
37            ),
38
39            ResponseError::NotUnique { .. } => Self::new(
40                ErrorKind::Query(QueryErrorKind::NotUnique),
41                ErrorOrigin::Response,
42                err.to_string(),
43            ),
44        }
45    }
46
47    #[must_use]
48    pub const fn kind(&self) -> &ErrorKind {
49        &self.kind
50    }
51
52    #[must_use]
53    pub const fn origin(&self) -> ErrorOrigin {
54        self.origin
55    }
56
57    #[must_use]
58    pub fn message(&self) -> &str {
59        &self.message
60    }
61}
62
63impl From<InternalError> for Error {
64    fn from(err: InternalError) -> Self {
65        Self::new(
66            ErrorKind::Runtime(map_class(err.class())),
67            err.origin().into(),
68            err.into_message(),
69        )
70    }
71}
72
73impl From<QueryError> for Error {
74    fn from(err: QueryError) -> Self {
75        match err {
76            QueryError::Validate(_) => Self::new(
77                ErrorKind::Query(QueryErrorKind::Validate),
78                ErrorOrigin::Query,
79                err.to_string(),
80            ),
81
82            QueryError::Intent(_) => Self::new(
83                ErrorKind::Query(QueryErrorKind::Intent),
84                ErrorOrigin::Query,
85                err.to_string(),
86            ),
87
88            QueryError::Plan(ref plan) => {
89                let kind = if plan.as_ref().is_unordered_pagination() {
90                    QueryErrorKind::UnorderedPagination
91                } else {
92                    QueryErrorKind::Plan
93                };
94
95                Self::new(ErrorKind::Query(kind), ErrorOrigin::Query, err.to_string())
96            }
97
98            QueryError::Response(err) => Self::from_response_error(err),
99
100            QueryError::Execute(err) => match err {
101                QueryExecutionError::Corruption(inner)
102                | QueryExecutionError::IncompatiblePersistedFormat(inner)
103                | QueryExecutionError::InvariantViolation(inner)
104                | QueryExecutionError::Conflict(inner)
105                | QueryExecutionError::NotFound(inner)
106                | QueryExecutionError::Unsupported(inner)
107                | QueryExecutionError::Internal(inner) => inner.into(),
108            },
109        }
110    }
111}
112
113const fn map_class(class: CoreErrorClass) -> RuntimeErrorKind {
114    match class {
115        CoreErrorClass::Corruption => RuntimeErrorKind::Corruption,
116        CoreErrorClass::IncompatiblePersistedFormat => {
117            RuntimeErrorKind::IncompatiblePersistedFormat
118        }
119        CoreErrorClass::InvariantViolation => RuntimeErrorKind::InvariantViolation,
120        CoreErrorClass::Conflict => RuntimeErrorKind::Conflict,
121        CoreErrorClass::NotFound => RuntimeErrorKind::NotFound,
122        CoreErrorClass::Unsupported => RuntimeErrorKind::Unsupported,
123        CoreErrorClass::Internal => RuntimeErrorKind::Internal,
124    }
125}
126
127impl From<ResponseError> for Error {
128    fn from(err: ResponseError) -> Self {
129        Self::from_response_error(err)
130    }
131}
132
133#[cfg_attr(doc, doc = "ErrorKind\n\nPublic error category.")]
134#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
135pub enum ErrorKind {
136    Query(QueryErrorKind),
137
138    /// Runtime failure.
139    Runtime(RuntimeErrorKind),
140}
141
142#[cfg_attr(doc, doc = "RuntimeErrorKind\n\nPublic runtime error class.")]
143#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
144pub enum RuntimeErrorKind {
145    Corruption,
146    IncompatiblePersistedFormat,
147    InvariantViolation,
148    Conflict,
149    NotFound,
150    Unsupported,
151    Internal,
152}
153
154#[cfg_attr(doc, doc = "QueryErrorKind\n\nPublic query error class.")]
155#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
156pub enum QueryErrorKind {
157    /// Validation failed.
158    Validate,
159
160    /// Intent validation failed.
161    Intent,
162
163    /// Planning failed.
164    Plan,
165
166    /// Pagination lacked ordering.
167    UnorderedPagination,
168
169    /// Continuation cursor was invalid.
170    InvalidContinuationCursor,
171
172    /// No rows matched.
173    NotFound,
174
175    /// More than one row matched.
176    NotUnique,
177}
178
179#[cfg_attr(doc, doc = "ErrorOrigin\n\nPublic error origin.")]
180#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
181pub enum ErrorOrigin {
182    Cursor,
183    Executor,
184    Identity,
185    Index,
186    Interface,
187    Planner,
188    Query,
189    Recovery,
190    Response,
191    Serialize,
192    Store,
193}
194
195impl From<CoreErrorOrigin> for ErrorOrigin {
196    fn from(origin: CoreErrorOrigin) -> Self {
197        match origin {
198            CoreErrorOrigin::Cursor => Self::Cursor,
199            CoreErrorOrigin::Executor => Self::Executor,
200            CoreErrorOrigin::Identity => Self::Identity,
201            CoreErrorOrigin::Index => Self::Index,
202            CoreErrorOrigin::Interface => Self::Interface,
203            CoreErrorOrigin::Planner => Self::Planner,
204            CoreErrorOrigin::Query => Self::Query,
205            CoreErrorOrigin::Recovery => Self::Recovery,
206            CoreErrorOrigin::Response => Self::Response,
207            CoreErrorOrigin::Serialize => Self::Serialize,
208            CoreErrorOrigin::Store => Self::Store,
209        }
210    }
211}
212
213//
214// TESTS
215//
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use candid::types::{CandidType, Label, Type, TypeInner};
221    use icydb_core::db::{IntentError, PlanError, ValidateError};
222    use icydb_core::error::{ErrorClass as CoreErrorClass, ErrorOrigin as CoreErrorOrigin};
223
224    fn expect_record_fields(ty: Type) -> Vec<String> {
225        match ty.as_ref() {
226            TypeInner::Record(fields) => fields
227                .iter()
228                .map(|field| match field.id.as_ref() {
229                    Label::Named(name) => name.clone(),
230                    other => panic!("expected named record field, got {other:?}"),
231                })
232                .collect(),
233            other => panic!("expected candid record, got {other:?}"),
234        }
235    }
236
237    fn expect_variant_labels(ty: Type) -> Vec<String> {
238        match ty.as_ref() {
239            TypeInner::Variant(fields) => fields
240                .iter()
241                .map(|field| match field.id.as_ref() {
242                    Label::Named(name) => name.clone(),
243                    other => panic!("expected named variant label, got {other:?}"),
244                })
245                .collect(),
246            other => panic!("expected candid variant, got {other:?}"),
247        }
248    }
249
250    #[test]
251    fn query_validate_maps_to_validate_kind() {
252        let err = QueryError::Validate(Box::new(ValidateError::UnknownField {
253            field: "field".to_string(),
254        }));
255        let facade = Error::from(err);
256
257        assert_eq!(facade.kind(), &ErrorKind::Query(QueryErrorKind::Validate));
258        assert_eq!(facade.origin(), ErrorOrigin::Query);
259    }
260
261    #[test]
262    fn query_intent_maps_to_intent_kind() {
263        let err = QueryError::Intent(IntentError::ByIdsWithPredicate);
264        let facade = Error::from(err);
265
266        assert_eq!(facade.kind(), &ErrorKind::Query(QueryErrorKind::Intent));
267        assert_eq!(facade.origin(), ErrorOrigin::Query);
268    }
269
270    #[test]
271    fn plan_errors_map_to_plan_kind() {
272        let err = QueryError::Plan(Box::new(PlanError::from(ValidateError::UnknownField {
273            field: "field".to_string(),
274        })));
275        let facade = Error::from(err);
276
277        assert_eq!(facade.kind(), &ErrorKind::Query(QueryErrorKind::Plan));
278        assert_eq!(facade.origin(), ErrorOrigin::Query);
279    }
280
281    #[test]
282    fn response_error_maps_with_response_origin() {
283        let facade = Error::from(ResponseError::NotFound { entity: "Entity" });
284
285        assert_eq!(facade.kind(), &ErrorKind::Query(QueryErrorKind::NotFound));
286        assert_eq!(facade.origin(), ErrorOrigin::Response);
287    }
288
289    #[test]
290    fn internal_error_class_matrix_maps_to_runtime_kind_and_preserves_origin() {
291        let cases = [
292            (CoreErrorClass::Corruption, RuntimeErrorKind::Corruption),
293            (
294                CoreErrorClass::IncompatiblePersistedFormat,
295                RuntimeErrorKind::IncompatiblePersistedFormat,
296            ),
297            (
298                CoreErrorClass::InvariantViolation,
299                RuntimeErrorKind::InvariantViolation,
300            ),
301            (CoreErrorClass::Conflict, RuntimeErrorKind::Conflict),
302            (CoreErrorClass::NotFound, RuntimeErrorKind::NotFound),
303            (CoreErrorClass::Unsupported, RuntimeErrorKind::Unsupported),
304            (CoreErrorClass::Internal, RuntimeErrorKind::Internal),
305        ];
306
307        for (class, expected_kind) in cases {
308            let core_err = InternalError::new(class, CoreErrorOrigin::Index, "runtime failure");
309            let facade = Error::from(core_err);
310
311            assert_eq!(facade.kind(), &ErrorKind::Runtime(expected_kind));
312            assert_eq!(facade.origin(), ErrorOrigin::Index);
313        }
314    }
315
316    #[test]
317    fn query_execute_preserves_runtime_class_and_origin() {
318        let cases = [
319            (
320                CoreErrorClass::Conflict,
321                CoreErrorOrigin::Store,
322                RuntimeErrorKind::Conflict,
323                ErrorOrigin::Store,
324                "write conflict",
325            ),
326            (
327                CoreErrorClass::NotFound,
328                CoreErrorOrigin::Executor,
329                RuntimeErrorKind::NotFound,
330                ErrorOrigin::Executor,
331                "row missing",
332            ),
333            (
334                CoreErrorClass::Internal,
335                CoreErrorOrigin::Planner,
336                RuntimeErrorKind::Internal,
337                ErrorOrigin::Planner,
338                "planner internal",
339            ),
340            (
341                CoreErrorClass::Unsupported,
342                CoreErrorOrigin::Query,
343                RuntimeErrorKind::Unsupported,
344                ErrorOrigin::Query,
345                "unsupported SQL feature",
346            ),
347        ];
348
349        for (class, origin, expected_kind, expected_origin, message) in cases {
350            let query_err = QueryError::Execute(QueryExecutionError::from(InternalError::new(
351                class, origin, message,
352            )));
353            let facade = Error::from(query_err);
354
355            assert_eq!(facade.kind(), &ErrorKind::Runtime(expected_kind));
356            assert_eq!(facade.origin(), expected_origin);
357        }
358    }
359
360    #[test]
361    fn query_execute_storage_and_index_origins_map_to_runtime_contract() {
362        let cases = [
363            (
364                CoreErrorClass::Internal,
365                CoreErrorOrigin::Store,
366                RuntimeErrorKind::Internal,
367                ErrorOrigin::Store,
368                "store internal",
369            ),
370            (
371                CoreErrorClass::Corruption,
372                CoreErrorOrigin::Index,
373                RuntimeErrorKind::Corruption,
374                ErrorOrigin::Index,
375                "index corruption",
376            ),
377            (
378                CoreErrorClass::Unsupported,
379                CoreErrorOrigin::Store,
380                RuntimeErrorKind::Unsupported,
381                ErrorOrigin::Store,
382                "store unsupported",
383            ),
384            (
385                CoreErrorClass::IncompatiblePersistedFormat,
386                CoreErrorOrigin::Serialize,
387                RuntimeErrorKind::IncompatiblePersistedFormat,
388                ErrorOrigin::Serialize,
389                "incompatible persisted format",
390            ),
391        ];
392
393        for (class, origin, expected_kind, expected_origin, message) in cases {
394            let query_err = QueryError::Execute(QueryExecutionError::from(InternalError::new(
395                class, origin, message,
396            )));
397            let facade = Error::from(query_err);
398
399            assert_eq!(facade.kind(), &ErrorKind::Runtime(expected_kind));
400            assert_eq!(facade.origin(), expected_origin);
401        }
402    }
403
404    #[test]
405    fn origin_mapping_includes_new_core_domains() {
406        let cases = [
407            (CoreErrorOrigin::Cursor, ErrorOrigin::Cursor),
408            (CoreErrorOrigin::Planner, ErrorOrigin::Planner),
409            (CoreErrorOrigin::Recovery, ErrorOrigin::Recovery),
410            (CoreErrorOrigin::Identity, ErrorOrigin::Identity),
411        ];
412
413        for (origin, expected) in cases {
414            let facade = Error::from(InternalError::new(
415                CoreErrorClass::Internal,
416                origin,
417                "origin mapping",
418            ));
419            assert_eq!(facade.origin(), expected);
420        }
421    }
422
423    #[test]
424    fn error_struct_candid_shape_is_stable() {
425        let fields = expect_record_fields(Error::ty());
426
427        for field in ["kind", "origin", "message"] {
428            assert!(
429                fields.iter().any(|candidate| candidate == field),
430                "Error must keep `{field}` as Candid field key",
431            );
432        }
433    }
434
435    #[test]
436    fn error_kind_candid_shape_is_stable() {
437        let labels = expect_variant_labels(ErrorKind::ty());
438        assert!(
439            labels.iter().any(|candidate| candidate == "Runtime"),
440            "ErrorKind must keep `Runtime` variant label",
441        );
442    }
443
444    #[test]
445    fn runtime_error_and_origin_variant_labels_are_stable() {
446        let runtime_labels = expect_variant_labels(RuntimeErrorKind::ty());
447        assert!(
448            runtime_labels
449                .iter()
450                .any(|candidate| candidate == "InvariantViolation"),
451            "RuntimeErrorKind must keep `InvariantViolation` variant label",
452        );
453
454        let origin_labels = expect_variant_labels(ErrorOrigin::ty());
455        assert!(
456            origin_labels
457                .iter()
458                .any(|candidate| candidate == "Serialize"),
459            "ErrorOrigin must keep `Serialize` variant label",
460        );
461    }
462
463    #[test]
464    fn query_error_kind_variant_labels_are_stable() {
465        let labels = expect_variant_labels(QueryErrorKind::ty());
466
467        for label in [
468            "Validate",
469            "Intent",
470            "Plan",
471            "UnorderedPagination",
472            "InvalidContinuationCursor",
473            "NotFound",
474            "NotUnique",
475        ] {
476            assert!(
477                labels.iter().any(|candidate| candidate == label),
478                "QueryErrorKind must keep `{label}` variant label",
479            );
480        }
481    }
482}