Skip to main content

icydb/
error.rs

1//! Module: error
2//!
3//! Responsibility: public typed error payloads and facade error category mapping.
4//! Does not own: internal error construction or accepted constraint authority.
5//! Boundary: maps core diagnostics to canister-facing Candid error records.
6
7#[cfg(test)]
8mod tests;
9
10use std::fmt;
11
12use candid::CandidType;
13use icydb_core::db::QueryError;
14use icydb_core::error::{ErrorOrigin as CoreErrorOrigin, InternalError};
15use serde::Deserialize;
16
17use crate::db::DatabaseBootstrapError;
18
19pub use icydb_core::error::{
20    ConstraintValidationFindingOutput, ConstraintValuePath, ConstraintValuePathComponent,
21};
22
23//
24// DiagnosticFact
25//
26
27/// One bounded numeric parameter attached to a public IcyDB error.
28///
29/// The leaf error code owns the reason. Facts contain only production-safe
30/// numeric parameters interpreted together with that code.
31#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
32pub struct DiagnosticFact {
33    tag: u8,
34    value: u64,
35}
36
37impl DiagnosticFact {
38    const fn from_numeric(tag: icydb_diagnostic_code::DiagnosticFactTag, value: u64) -> Self {
39        Self {
40            tag: tag.raw(),
41            value,
42        }
43    }
44
45    /// Return the stable numeric fact-tag identity.
46    #[must_use]
47    pub const fn tag(&self) -> u8 {
48        self.tag
49    }
50
51    /// Return the numeric fact value.
52    #[must_use]
53    pub const fn value(&self) -> u64 {
54        self.value
55    }
56}
57
58//
59// QueryFieldDiagnostic
60//
61
62/// One bounded rejected query-field reference attached to a planning error.
63#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
64pub struct QueryFieldDiagnostic {
65    role: u8,
66    field: String,
67}
68
69impl QueryFieldDiagnostic {
70    fn from_core(role: icydb_diagnostic_code::QueryFieldRole, field: &str) -> Self {
71        Self {
72            role: role.raw(),
73            field: field.to_owned(),
74        }
75    }
76
77    /// Return the raw compact role identity.
78    #[must_use]
79    pub const fn role(&self) -> u8 {
80        self.role
81    }
82
83    /// Return the known typed role, or `None` for malformed decoded context.
84    #[must_use]
85    pub const fn known_role(&self) -> Option<icydb_diagnostic_code::QueryFieldRole> {
86        icydb_diagnostic_code::QueryFieldRole::known(self.role)
87    }
88
89    /// Borrow the exact post-normalization rejected field reference.
90    #[must_use]
91    pub const fn field(&self) -> &str {
92        self.field.as_str()
93    }
94}
95
96//
97// Error
98//
99
100#[cfg_attr(doc, doc = "Error\n\nPublic error payload.")]
101#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
102pub struct Error {
103    code: u16,
104    class: u8,
105    origin: u8,
106    facts: Vec<DiagnosticFact>,
107    query_field: Option<QueryFieldDiagnostic>,
108}
109
110impl Error {
111    /// Build a compact public error from one diagnostic code and origin.
112    #[must_use]
113    pub const fn from_code(
114        code: icydb_diagnostic_code::DiagnosticCode,
115        origin: ErrorOrigin,
116    ) -> Self {
117        Self::from_error_code(code.error_code(), origin)
118    }
119
120    /// Build a public error from one numeric wire code and origin.
121    #[must_use]
122    pub const fn from_error_code(
123        code: icydb_diagnostic_code::ErrorCode,
124        origin: ErrorOrigin,
125    ) -> Self {
126        Self {
127            code: code.raw(),
128            class: code.class().wire_code(),
129            origin: origin.wire_code(),
130            facts: Vec::new(),
131            query_field: None,
132        }
133    }
134
135    /// Build a compact public error from one public category and origin.
136    ///
137    /// This helper keeps generated endpoint code concise while the wire
138    /// payload itself remains code/detail-first.
139    #[must_use]
140    pub const fn from_kind(kind: ErrorKind, origin: ErrorOrigin) -> Self {
141        let code = kind.diagnostic_code();
142        let error_code =
143            icydb_diagnostic_code::ErrorCode::from_parts(code, Some(kind.diagnostic_detail()));
144
145        Self::from_error_code(error_code, origin)
146    }
147
148    /// Build a compact public runtime-boundary error.
149    #[must_use]
150    pub const fn from_runtime_boundary(
151        boundary: icydb_diagnostic_code::RuntimeBoundaryCode,
152        origin: ErrorOrigin,
153    ) -> Self {
154        let detail = icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary { boundary };
155        let error_code =
156            icydb_diagnostic_code::ErrorCode::from_parts(detail.diagnostic_code(), Some(detail));
157
158        Self::from_error_code(error_code, origin)
159    }
160
161    /// Build a compact public error from a full diagnostic payload.
162    #[must_use]
163    pub fn from_diagnostic(diagnostic: icydb_diagnostic_code::Diagnostic) -> Self {
164        Self::from_error_code(diagnostic.error_code(), diagnostic.origin().into())
165    }
166
167    fn from_internal_error(err: &InternalError) -> Self {
168        Self::from_diagnostic_and_facts(err.diagnostic(), err.diagnostic_facts())
169    }
170
171    pub(crate) fn from_diagnostic_and_facts(
172        diagnostic: icydb_diagnostic_code::Diagnostic,
173        facts: Vec<(icydb_diagnostic_code::DiagnosticFactTag, u64)>,
174    ) -> Self {
175        Self::from_diagnostic_facts_and_query_field(diagnostic, facts, None)
176    }
177
178    fn from_diagnostic_facts_and_query_field(
179        diagnostic: icydb_diagnostic_code::Diagnostic,
180        facts: Vec<(icydb_diagnostic_code::DiagnosticFactTag, u64)>,
181        query_field: Option<(icydb_diagnostic_code::QueryFieldRole, &str)>,
182    ) -> Self {
183        if icydb_diagnostic_code::validate_known_diagnostic_fact_schema(
184            diagnostic.error_code(),
185            facts.as_slice(),
186        )
187        .is_err()
188        {
189            return Self::from_kind(
190                ErrorKind::Runtime(RuntimeErrorKind::InvariantViolation),
191                diagnostic.origin().into(),
192            );
193        }
194
195        if let Some((role, field)) = query_field
196            && icydb_diagnostic_code::validate_query_field_schema(
197                diagnostic.error_code(),
198                role.raw(),
199                field,
200            )
201            .is_err()
202        {
203            return Self::from_kind(
204                ErrorKind::Runtime(RuntimeErrorKind::InvariantViolation),
205                diagnostic.origin().into(),
206            );
207        }
208
209        let mut error = Self::from_diagnostic(diagnostic);
210        error.facts = facts
211            .into_iter()
212            .map(|(tag, value)| DiagnosticFact::from_numeric(tag, value))
213            .collect();
214        error.query_field =
215            query_field.map(|(role, field)| QueryFieldDiagnostic::from_core(role, field));
216        error
217    }
218
219    /// Return the compact diagnostic code.
220    #[must_use]
221    pub const fn code(&self) -> icydb_diagnostic_code::ErrorCode {
222        icydb_diagnostic_code::ErrorCode::from_raw(self.code)
223    }
224
225    /// Return the broad compact diagnostic class.
226    #[must_use]
227    pub const fn class(&self) -> icydb_diagnostic_code::ErrorClass {
228        match icydb_diagnostic_code::ErrorClass::from_wire_code(self.class) {
229            Some(class) => class,
230            None => self.code().class(),
231        }
232    }
233
234    /// Return the diagnostic origin.
235    #[must_use]
236    pub const fn origin(&self) -> ErrorOrigin {
237        ErrorOrigin::from_wire_code(self.origin)
238    }
239
240    /// Return compact diagnostic identity for this error.
241    #[must_use]
242    pub fn diagnostic(&self) -> icydb_diagnostic_code::Diagnostic {
243        self.code().diagnostic(self.origin().into())
244    }
245
246    /// Return the compact diagnostic code for this error.
247    #[must_use]
248    pub const fn diagnostic_code(&self) -> icydb_diagnostic_code::DiagnosticCode {
249        self.code().diagnostic_code()
250    }
251
252    /// Borrow the bounded numeric facts carried by this error.
253    #[must_use]
254    pub const fn facts(&self) -> &[DiagnosticFact] {
255        self.facts.as_slice()
256    }
257
258    /// Borrow the raw optional query-field context.
259    #[must_use]
260    pub const fn query_field(&self) -> Option<&QueryFieldDiagnostic> {
261        self.query_field.as_ref()
262    }
263
264    /// Validate and borrow the optional query-field context.
265    ///
266    /// Decoded client data remains untrusted until this check succeeds.
267    pub fn validated_query_field(
268        &self,
269    ) -> Result<
270        Option<(icydb_diagnostic_code::QueryFieldRole, &str)>,
271        icydb_diagnostic_code::QueryFieldSchemaMismatch,
272    > {
273        let Some(context) = self.query_field.as_ref() else {
274            return Ok(None);
275        };
276        let role = icydb_diagnostic_code::validate_query_field_schema(
277            self.code(),
278            context.role,
279            context.field.as_str(),
280        )?;
281
282        Ok(Some((role, context.field.as_str())))
283    }
284
285    pub(crate) fn core_facts(
286        &self,
287    ) -> Option<Vec<(icydb_diagnostic_code::DiagnosticFactTag, u64)>> {
288        self.facts
289            .iter()
290            .map(|fact| {
291                icydb_diagnostic_code::DiagnosticFactTag::known(fact.tag)
292                    .map(|tag| (tag, fact.value))
293            })
294            .collect()
295    }
296}
297
298impl From<InternalError> for Error {
299    fn from(err: InternalError) -> Self {
300        Self::from_internal_error(&err)
301    }
302}
303
304impl From<QueryError> for Error {
305    fn from(err: QueryError) -> Self {
306        Self::from_diagnostic_facts_and_query_field(
307            err.diagnostic(),
308            err.diagnostic_facts(),
309            err.query_field_context(),
310        )
311    }
312}
313
314impl From<DatabaseBootstrapError> for Error {
315    fn from(_err: DatabaseBootstrapError) -> Self {
316        Self::from_kind(
317            ErrorKind::Runtime(RuntimeErrorKind::Internal),
318            ErrorOrigin::Runtime,
319        )
320    }
321}
322
323impl fmt::Display for Error {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        write!(f, "E{}", self.code)
326    }
327}
328
329impl std::error::Error for Error {}
330
331#[cfg_attr(doc, doc = "ErrorKind\n\nPublic error category.")]
332#[derive(Clone, Copy, Debug, Eq, PartialEq)]
333pub enum ErrorKind {
334    Query(QueryErrorKind),
335
336    /// Runtime failure.
337    Runtime(RuntimeErrorKind),
338}
339
340impl ErrorKind {
341    /// Return the compact diagnostic code for this public error category.
342    #[must_use]
343    pub const fn diagnostic_code(self) -> icydb_diagnostic_code::DiagnosticCode {
344        match self {
345            Self::Query(kind) => kind.diagnostic_code(),
346            Self::Runtime(kind) => kind.diagnostic_code(),
347        }
348    }
349
350    const fn diagnostic_detail(self) -> icydb_diagnostic_code::DiagnosticDetail {
351        match self {
352            Self::Query(kind) => icydb_diagnostic_code::DiagnosticDetail::QueryKind {
353                kind: kind.diagnostic_kind(),
354            },
355            Self::Runtime(kind) => icydb_diagnostic_code::DiagnosticDetail::RuntimeKind {
356                kind: kind.diagnostic_kind(),
357            },
358        }
359    }
360}
361
362#[cfg_attr(doc, doc = "RuntimeErrorKind\n\nPublic runtime error class.")]
363#[derive(Clone, Copy, Debug, Eq, PartialEq)]
364pub enum RuntimeErrorKind {
365    Corruption,
366    IncompatiblePersistedFormat,
367    InvariantViolation,
368    Conflict,
369    NotFound,
370    Unsupported,
371    Internal,
372}
373
374impl RuntimeErrorKind {
375    /// Return the compact diagnostic code for this runtime category.
376    #[must_use]
377    pub const fn diagnostic_code(self) -> icydb_diagnostic_code::DiagnosticCode {
378        match self {
379            Self::Corruption => icydb_diagnostic_code::DiagnosticCode::RuntimeCorruption,
380            Self::IncompatiblePersistedFormat => {
381                icydb_diagnostic_code::DiagnosticCode::RuntimeIncompatiblePersistedFormat
382            }
383            Self::InvariantViolation => {
384                icydb_diagnostic_code::DiagnosticCode::RuntimeInvariantViolation
385            }
386            Self::Conflict => icydb_diagnostic_code::DiagnosticCode::RuntimeConflict,
387            Self::NotFound => icydb_diagnostic_code::DiagnosticCode::RuntimeNotFound,
388            Self::Unsupported => icydb_diagnostic_code::DiagnosticCode::RuntimeUnsupported,
389            Self::Internal => icydb_diagnostic_code::DiagnosticCode::RuntimeInternal,
390        }
391    }
392
393    const fn diagnostic_kind(self) -> icydb_diagnostic_code::RuntimeErrorKind {
394        match self {
395            Self::Corruption => icydb_diagnostic_code::RuntimeErrorKind::Corruption,
396            Self::IncompatiblePersistedFormat => {
397                icydb_diagnostic_code::RuntimeErrorKind::IncompatiblePersistedFormat
398            }
399            Self::InvariantViolation => icydb_diagnostic_code::RuntimeErrorKind::InvariantViolation,
400            Self::Conflict => icydb_diagnostic_code::RuntimeErrorKind::Conflict,
401            Self::NotFound => icydb_diagnostic_code::RuntimeErrorKind::NotFound,
402            Self::Unsupported => icydb_diagnostic_code::RuntimeErrorKind::Unsupported,
403            Self::Internal => icydb_diagnostic_code::RuntimeErrorKind::Internal,
404        }
405    }
406}
407
408#[cfg_attr(doc, doc = "QueryErrorKind\n\nPublic query error class.")]
409#[derive(Clone, Copy, Debug, Eq, PartialEq)]
410pub enum QueryErrorKind {
411    /// Validation failed.
412    Validate,
413
414    /// Intent validation failed.
415    Intent,
416
417    /// Planning failed.
418    Plan,
419
420    /// Pagination lacked ordering.
421    UnorderedPagination,
422
423    /// Continuation cursor was invalid.
424    InvalidContinuationCursor,
425
426    /// No rows matched.
427    NotFound,
428
429    /// More than one row matched.
430    NotUnique,
431}
432
433impl QueryErrorKind {
434    /// Return the compact diagnostic code for this query category.
435    #[must_use]
436    pub const fn diagnostic_code(self) -> icydb_diagnostic_code::DiagnosticCode {
437        match self {
438            Self::Validate => icydb_diagnostic_code::DiagnosticCode::QueryValidate,
439            Self::Intent => icydb_diagnostic_code::DiagnosticCode::QueryIntent,
440            Self::Plan => icydb_diagnostic_code::DiagnosticCode::QueryPlan,
441            Self::UnorderedPagination => {
442                icydb_diagnostic_code::DiagnosticCode::QueryUnorderedPagination
443            }
444            Self::InvalidContinuationCursor => {
445                icydb_diagnostic_code::DiagnosticCode::QueryInvalidContinuationCursor
446            }
447            Self::NotFound => icydb_diagnostic_code::DiagnosticCode::QueryNotFound,
448            Self::NotUnique => icydb_diagnostic_code::DiagnosticCode::QueryNotUnique,
449        }
450    }
451
452    const fn diagnostic_kind(self) -> icydb_diagnostic_code::QueryErrorKind {
453        match self {
454            Self::Validate => icydb_diagnostic_code::QueryErrorKind::Validate,
455            Self::Intent => icydb_diagnostic_code::QueryErrorKind::Intent,
456            Self::Plan => icydb_diagnostic_code::QueryErrorKind::Plan,
457            Self::UnorderedPagination => icydb_diagnostic_code::QueryErrorKind::UnorderedPagination,
458            Self::InvalidContinuationCursor => {
459                icydb_diagnostic_code::QueryErrorKind::InvalidContinuationCursor
460            }
461            Self::NotFound => icydb_diagnostic_code::QueryErrorKind::NotFound,
462            Self::NotUnique => icydb_diagnostic_code::QueryErrorKind::NotUnique,
463        }
464    }
465}
466
467#[cfg_attr(doc, doc = "ErrorOrigin\n\nPublic error origin.")]
468#[derive(Clone, Copy, Debug, Eq, PartialEq)]
469pub enum ErrorOrigin {
470    Cursor,
471    Executor,
472    Identity,
473    Index,
474    Interface,
475    Planner,
476    Query,
477    Recovery,
478    Response,
479    Runtime,
480    Serialize,
481    Store,
482}
483
484impl ErrorOrigin {
485    const fn wire_code(self) -> u8 {
486        match self {
487            Self::Cursor => icydb_diagnostic_code::ErrorOrigin::Cursor.wire_code(),
488            Self::Executor => icydb_diagnostic_code::ErrorOrigin::Executor.wire_code(),
489            Self::Identity => icydb_diagnostic_code::ErrorOrigin::Identity.wire_code(),
490            Self::Index => icydb_diagnostic_code::ErrorOrigin::Index.wire_code(),
491            Self::Interface => icydb_diagnostic_code::ErrorOrigin::Interface.wire_code(),
492            Self::Planner => icydb_diagnostic_code::ErrorOrigin::Planner.wire_code(),
493            Self::Query => icydb_diagnostic_code::ErrorOrigin::Query.wire_code(),
494            Self::Recovery => icydb_diagnostic_code::ErrorOrigin::Recovery.wire_code(),
495            Self::Response => icydb_diagnostic_code::ErrorOrigin::Response.wire_code(),
496            Self::Runtime => icydb_diagnostic_code::ErrorOrigin::Runtime.wire_code(),
497            Self::Serialize => icydb_diagnostic_code::ErrorOrigin::Serialize.wire_code(),
498            Self::Store => icydb_diagnostic_code::ErrorOrigin::Store.wire_code(),
499        }
500    }
501
502    const fn from_wire_code(code: u8) -> Self {
503        match icydb_diagnostic_code::ErrorOrigin::from_wire_code(code) {
504            icydb_diagnostic_code::ErrorOrigin::Cursor => Self::Cursor,
505            icydb_diagnostic_code::ErrorOrigin::Executor => Self::Executor,
506            icydb_diagnostic_code::ErrorOrigin::Identity => Self::Identity,
507            icydb_diagnostic_code::ErrorOrigin::Index => Self::Index,
508            icydb_diagnostic_code::ErrorOrigin::Interface => Self::Interface,
509            icydb_diagnostic_code::ErrorOrigin::Planner => Self::Planner,
510            icydb_diagnostic_code::ErrorOrigin::Query => Self::Query,
511            icydb_diagnostic_code::ErrorOrigin::Recovery => Self::Recovery,
512            icydb_diagnostic_code::ErrorOrigin::Response => Self::Response,
513            icydb_diagnostic_code::ErrorOrigin::Runtime => Self::Runtime,
514            icydb_diagnostic_code::ErrorOrigin::Serialize => Self::Serialize,
515            icydb_diagnostic_code::ErrorOrigin::Store => Self::Store,
516        }
517    }
518}
519
520impl From<CoreErrorOrigin> for ErrorOrigin {
521    fn from(origin: CoreErrorOrigin) -> Self {
522        match origin {
523            CoreErrorOrigin::Cursor => Self::Cursor,
524            CoreErrorOrigin::Executor => Self::Executor,
525            CoreErrorOrigin::Identity => Self::Identity,
526            CoreErrorOrigin::Index => Self::Index,
527            CoreErrorOrigin::Interface => Self::Interface,
528            CoreErrorOrigin::Planner => Self::Planner,
529            CoreErrorOrigin::Query => Self::Query,
530            CoreErrorOrigin::Recovery => Self::Recovery,
531            CoreErrorOrigin::Response => Self::Response,
532            CoreErrorOrigin::Serialize => Self::Serialize,
533            CoreErrorOrigin::Store => Self::Store,
534        }
535    }
536}
537
538impl From<ErrorOrigin> for icydb_diagnostic_code::ErrorOrigin {
539    fn from(origin: ErrorOrigin) -> Self {
540        match origin {
541            ErrorOrigin::Cursor => Self::Cursor,
542            ErrorOrigin::Executor => Self::Executor,
543            ErrorOrigin::Identity => Self::Identity,
544            ErrorOrigin::Index => Self::Index,
545            ErrorOrigin::Interface => Self::Interface,
546            ErrorOrigin::Planner => Self::Planner,
547            ErrorOrigin::Query => Self::Query,
548            ErrorOrigin::Recovery => Self::Recovery,
549            ErrorOrigin::Response => Self::Response,
550            ErrorOrigin::Runtime => Self::Runtime,
551            ErrorOrigin::Serialize => Self::Serialize,
552            ErrorOrigin::Store => Self::Store,
553        }
554    }
555}
556
557impl From<icydb_diagnostic_code::ErrorOrigin> for ErrorOrigin {
558    fn from(origin: icydb_diagnostic_code::ErrorOrigin) -> Self {
559        match origin {
560            icydb_diagnostic_code::ErrorOrigin::Cursor => Self::Cursor,
561            icydb_diagnostic_code::ErrorOrigin::Executor => Self::Executor,
562            icydb_diagnostic_code::ErrorOrigin::Identity => Self::Identity,
563            icydb_diagnostic_code::ErrorOrigin::Index => Self::Index,
564            icydb_diagnostic_code::ErrorOrigin::Interface => Self::Interface,
565            icydb_diagnostic_code::ErrorOrigin::Planner => Self::Planner,
566            icydb_diagnostic_code::ErrorOrigin::Query => Self::Query,
567            icydb_diagnostic_code::ErrorOrigin::Recovery => Self::Recovery,
568            icydb_diagnostic_code::ErrorOrigin::Response => Self::Response,
569            icydb_diagnostic_code::ErrorOrigin::Runtime => Self::Runtime,
570            icydb_diagnostic_code::ErrorOrigin::Serialize => Self::Serialize,
571            icydb_diagnostic_code::ErrorOrigin::Store => Self::Store,
572        }
573    }
574}