Skip to main content

icydb_diagnostic_code/
lib.rs

1//! Compact diagnostic identity for IcyDB.
2//!
3//! This crate intentionally contains no rich diagnostic prose. Production
4//! canister builds can depend on these codes and structured details without
5//! linking CLI-oriented message text.
6
7///
8/// DiagnosticCode
9///
10/// Stable machine-readable diagnostic reason.
11///
12
13#[cfg_attr(
14    feature = "wire",
15    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
16)]
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18pub enum DiagnosticCode {
19    QueryValidate,
20    QueryIntent,
21    QueryPlan,
22    QueryAccessRequirement,
23    QueryUnorderedPagination,
24    QueryInvalidContinuationCursor,
25    QueryNotFound,
26    QueryNotUnique,
27    QueryNumericOverflow,
28    QueryNumericNotRepresentable,
29    QueryUnsupportedSqlFeature,
30    SchemaDdlAdmission,
31    StoreNotFound,
32    StoreCorruption,
33    StoreInvariantViolation,
34    RuntimeCorruption,
35    RuntimeIncompatiblePersistedFormat,
36    RuntimeInvariantViolation,
37    RuntimeConflict,
38    RuntimeNotFound,
39    RuntimeUnsupported,
40    RuntimeInternal,
41}
42
43impl DiagnosticCode {
44    /// Return the broad diagnostic class for this code.
45    #[must_use]
46    pub const fn class(self) -> ErrorClass {
47        match self {
48            Self::StoreCorruption | Self::RuntimeCorruption => ErrorClass::Corruption,
49            Self::RuntimeIncompatiblePersistedFormat => ErrorClass::IncompatiblePersistedFormat,
50            Self::QueryNotFound | Self::StoreNotFound | Self::RuntimeNotFound => {
51                ErrorClass::NotFound
52            }
53            Self::RuntimeConflict => ErrorClass::Conflict,
54            Self::QueryUnsupportedSqlFeature | Self::RuntimeUnsupported => ErrorClass::Unsupported,
55            Self::StoreInvariantViolation | Self::RuntimeInvariantViolation => {
56                ErrorClass::InvariantViolation
57            }
58            Self::RuntimeInternal => ErrorClass::Internal,
59            Self::QueryValidate
60            | Self::QueryIntent
61            | Self::QueryPlan
62            | Self::QueryAccessRequirement
63            | Self::QueryUnorderedPagination
64            | Self::QueryInvalidContinuationCursor
65            | Self::QueryNotUnique
66            | Self::QueryNumericOverflow
67            | Self::QueryNumericNotRepresentable
68            | Self::SchemaDdlAdmission => ErrorClass::Query,
69        }
70    }
71
72    /// Return the default diagnostic origin for this code.
73    #[must_use]
74    pub const fn origin(self) -> ErrorOrigin {
75        match self {
76            Self::StoreNotFound | Self::StoreCorruption | Self::StoreInvariantViolation => {
77                ErrorOrigin::Store
78            }
79            Self::RuntimeCorruption
80            | Self::RuntimeIncompatiblePersistedFormat
81            | Self::RuntimeInvariantViolation
82            | Self::RuntimeConflict
83            | Self::RuntimeNotFound
84            | Self::RuntimeUnsupported
85            | Self::RuntimeInternal => ErrorOrigin::Runtime,
86            Self::QueryValidate
87            | Self::QueryIntent
88            | Self::QueryPlan
89            | Self::QueryAccessRequirement
90            | Self::QueryUnorderedPagination
91            | Self::QueryInvalidContinuationCursor
92            | Self::QueryNotFound
93            | Self::QueryNotUnique
94            | Self::QueryNumericOverflow
95            | Self::QueryNumericNotRepresentable
96            | Self::QueryUnsupportedSqlFeature
97            | Self::SchemaDdlAdmission => ErrorOrigin::Query,
98        }
99    }
100}
101
102///
103/// ErrorClass
104///
105/// Broad diagnostic class used for recovery decisions.
106///
107
108#[cfg_attr(
109    feature = "wire",
110    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
111)]
112#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
113pub enum ErrorClass {
114    Query,
115    Corruption,
116    IncompatiblePersistedFormat,
117    NotFound,
118    Internal,
119    Conflict,
120    Unsupported,
121    InvariantViolation,
122}
123
124///
125/// ErrorOrigin
126///
127/// Subsystem that owns the diagnostic.
128///
129
130#[cfg_attr(
131    feature = "wire",
132    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
133)]
134#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
135pub enum ErrorOrigin {
136    Cursor,
137    Executor,
138    Identity,
139    Index,
140    Interface,
141    Planner,
142    Query,
143    Recovery,
144    Response,
145    Runtime,
146    Serialize,
147    Store,
148}
149
150///
151/// QueryErrorKind
152///
153/// Public query error category.
154///
155
156#[cfg_attr(
157    feature = "wire",
158    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
159)]
160#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
161pub enum QueryErrorKind {
162    Validate,
163    Intent,
164    Plan,
165    AccessRequirement,
166    UnorderedPagination,
167    InvalidContinuationCursor,
168    NotFound,
169    NotUnique,
170}
171
172///
173/// RuntimeErrorKind
174///
175/// Public runtime error category.
176///
177
178#[cfg_attr(
179    feature = "wire",
180    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
181)]
182#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
183pub enum RuntimeErrorKind {
184    Corruption,
185    IncompatiblePersistedFormat,
186    InvariantViolation,
187    Conflict,
188    NotFound,
189    Unsupported,
190    Internal,
191}
192
193///
194/// SqlFeatureCode
195///
196/// Compact SQL feature identifier used by unsupported-feature diagnostics.
197///
198
199#[cfg_attr(
200    feature = "wire",
201    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
202)]
203#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
204pub enum SqlFeatureCode {
205    AlterTableUnsupportedOperation,
206    ColumnAlias,
207    DescribeModifier,
208    DropStatementBeyondDropIndex,
209    Join,
210    MultiStatementSql,
211    OrderByUnsupportedForm,
212    ParameterBinding,
213    ParameterizedSchemaVersion,
214    ReturningUnsupportedShape,
215    ShowUnsupportedCommand,
216    UnionIntersectExcept,
217    WindowFunction,
218}
219
220///
221/// SchemaDdlAdmissionCode
222///
223/// Compact SQL DDL admission rejection reason.
224///
225
226#[cfg_attr(
227    feature = "wire",
228    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
229)]
230#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
231pub enum SchemaDdlAdmissionCode {
232    MissingExpectedSchemaVersion,
233    MissingNextSchemaVersion,
234    StaleExpectedSchemaVersion,
235    InvalidExpectedSchemaVersion,
236    InvalidNextSchemaVersion,
237    AcceptedSchemaChangeWithoutVersionBump,
238    EmptyVersionBump,
239    VersionGap,
240    VersionRollback,
241    FingerprintMethodMismatch,
242    UnsupportedTransitionClass,
243    PhysicalRunnerMissing,
244    ValidationFailed,
245    PublicationRaceLost,
246}
247
248///
249/// DiagnosticDetail
250///
251/// Small structured diagnostic payload for callers and CLI rendering.
252///
253
254#[cfg_attr(
255    feature = "wire",
256    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
257)]
258#[derive(Clone, Debug, Eq, PartialEq)]
259pub enum DiagnosticDetail {
260    QueryKind { kind: QueryErrorKind },
261    RuntimeKind { kind: RuntimeErrorKind },
262    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
263    UnsupportedSqlFeature { feature: SqlFeatureCode },
264}
265
266///
267/// Diagnostic
268///
269/// Compact public diagnostic payload.
270///
271
272#[cfg_attr(
273    feature = "wire",
274    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
275)]
276#[derive(Clone, Debug, Eq, PartialEq)]
277pub struct Diagnostic {
278    code: DiagnosticCode,
279    origin: ErrorOrigin,
280    detail: Option<DiagnosticDetail>,
281}
282
283impl Diagnostic {
284    /// Build a compact diagnostic from a code and optional structured detail.
285    #[must_use]
286    pub const fn new(
287        code: DiagnosticCode,
288        origin: ErrorOrigin,
289        detail: Option<DiagnosticDetail>,
290    ) -> Self {
291        Self {
292            code,
293            origin,
294            detail,
295        }
296    }
297
298    /// Build a compact diagnostic using the code's default origin.
299    #[must_use]
300    pub const fn from_code(code: DiagnosticCode) -> Self {
301        Self::new(code, code.origin(), None)
302    }
303
304    /// Return the stable diagnostic code.
305    #[must_use]
306    pub const fn code(&self) -> DiagnosticCode {
307        self.code
308    }
309
310    /// Return the diagnostic class.
311    #[must_use]
312    pub const fn class(&self) -> ErrorClass {
313        self.code.class()
314    }
315
316    /// Return the subsystem origin.
317    #[must_use]
318    pub const fn origin(&self) -> ErrorOrigin {
319        self.origin
320    }
321
322    /// Return structured diagnostic detail, when available.
323    #[must_use]
324    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
325        self.detail.as_ref()
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::{Diagnostic, DiagnosticCode, ErrorClass, ErrorOrigin};
332
333    #[test]
334    fn diagnostic_from_code_uses_default_origin() {
335        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
336
337        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
338        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
339    }
340
341    #[test]
342    fn diagnostic_code_reports_broad_class() {
343        assert_eq!(
344            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
345            ErrorClass::Unsupported
346        );
347        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
348        assert_eq!(
349            DiagnosticCode::StoreCorruption.class(),
350            ErrorClass::Corruption
351        );
352    }
353}