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