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#[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(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 Validate,
159
160 Intent,
162
163 Plan,
165
166 UnorderedPagination,
168
169 InvalidContinuationCursor,
171
172 NotFound,
174
175 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#[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}