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    QuerySqlSurfaceMismatch,
31    SchemaDdlAdmission,
32    StoreNotFound,
33    StoreCorruption,
34    StoreInvariantViolation,
35    RuntimeCorruption,
36    RuntimeIncompatiblePersistedFormat,
37    RuntimeInvariantViolation,
38    RuntimeConflict,
39    RuntimeNotFound,
40    RuntimeUnsupported,
41    RuntimeInternal,
42}
43
44impl DiagnosticCode {
45    /// Return the broad diagnostic class for this code.
46    #[must_use]
47    pub const fn class(self) -> ErrorClass {
48        match self {
49            Self::StoreCorruption | Self::RuntimeCorruption => ErrorClass::Corruption,
50            Self::RuntimeIncompatiblePersistedFormat => ErrorClass::IncompatiblePersistedFormat,
51            Self::QueryNotFound | Self::StoreNotFound | Self::RuntimeNotFound => {
52                ErrorClass::NotFound
53            }
54            Self::RuntimeConflict => ErrorClass::Conflict,
55            Self::QueryUnsupportedSqlFeature
56            | Self::QuerySqlSurfaceMismatch
57            | Self::RuntimeUnsupported => ErrorClass::Unsupported,
58            Self::StoreInvariantViolation | Self::RuntimeInvariantViolation => {
59                ErrorClass::InvariantViolation
60            }
61            Self::RuntimeInternal => ErrorClass::Internal,
62            Self::QueryValidate
63            | Self::QueryIntent
64            | Self::QueryPlan
65            | Self::QueryAccessRequirement
66            | Self::QueryUnorderedPagination
67            | Self::QueryInvalidContinuationCursor
68            | Self::QueryNotUnique
69            | Self::QueryNumericOverflow
70            | Self::QueryNumericNotRepresentable
71            | Self::SchemaDdlAdmission => ErrorClass::Query,
72        }
73    }
74
75    /// Return the default diagnostic origin for this code.
76    #[must_use]
77    pub const fn origin(self) -> ErrorOrigin {
78        match self {
79            Self::StoreNotFound | Self::StoreCorruption | Self::StoreInvariantViolation => {
80                ErrorOrigin::Store
81            }
82            Self::RuntimeCorruption
83            | Self::RuntimeIncompatiblePersistedFormat
84            | Self::RuntimeInvariantViolation
85            | Self::RuntimeConflict
86            | Self::RuntimeNotFound
87            | Self::RuntimeUnsupported
88            | Self::RuntimeInternal => ErrorOrigin::Runtime,
89            Self::QueryValidate
90            | Self::QueryIntent
91            | Self::QueryPlan
92            | Self::QueryAccessRequirement
93            | Self::QueryUnorderedPagination
94            | Self::QueryInvalidContinuationCursor
95            | Self::QueryNotFound
96            | Self::QueryNotUnique
97            | Self::QueryNumericOverflow
98            | Self::QueryNumericNotRepresentable
99            | Self::QueryUnsupportedSqlFeature
100            | Self::QuerySqlSurfaceMismatch
101            | Self::SchemaDdlAdmission => ErrorOrigin::Query,
102        }
103    }
104}
105
106///
107/// ErrorClass
108///
109/// Broad diagnostic class used for recovery decisions.
110///
111
112#[cfg_attr(
113    feature = "wire",
114    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
115)]
116#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
117pub enum ErrorClass {
118    Query,
119    Corruption,
120    IncompatiblePersistedFormat,
121    NotFound,
122    Internal,
123    Conflict,
124    Unsupported,
125    InvariantViolation,
126}
127
128///
129/// ErrorOrigin
130///
131/// Subsystem that owns the diagnostic.
132///
133
134#[cfg_attr(
135    feature = "wire",
136    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
137)]
138#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
139pub enum ErrorOrigin {
140    Cursor,
141    Executor,
142    Identity,
143    Index,
144    Interface,
145    Planner,
146    Query,
147    Recovery,
148    Response,
149    Runtime,
150    Serialize,
151    Store,
152}
153
154///
155/// QueryErrorKind
156///
157/// Public query error category.
158///
159
160#[cfg_attr(
161    feature = "wire",
162    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
163)]
164#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
165pub enum QueryErrorKind {
166    Validate,
167    Intent,
168    Plan,
169    AccessRequirement,
170    UnorderedPagination,
171    InvalidContinuationCursor,
172    NotFound,
173    NotUnique,
174}
175
176///
177/// RuntimeErrorKind
178///
179/// Public runtime error category.
180///
181
182#[cfg_attr(
183    feature = "wire",
184    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
185)]
186#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
187pub enum RuntimeErrorKind {
188    Corruption,
189    IncompatiblePersistedFormat,
190    InvariantViolation,
191    Conflict,
192    NotFound,
193    Unsupported,
194    Internal,
195}
196
197///
198/// SqlFeatureCode
199///
200/// Compact SQL feature identifier used by unsupported-feature diagnostics.
201///
202
203#[cfg_attr(
204    feature = "wire",
205    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
206)]
207#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
208pub enum SqlFeatureCode {
209    AggregateFilterClause,
210    AlterStatementBeyondAlterTable,
211    AlterTableAddColumnDuplicateDefault,
212    AlterTableAddColumnModifiers,
213    AlterTableAddStatementBeyondAddColumn,
214    AlterTableAlterColumnDropUnsupportedAction,
215    AlterTableAlterColumnModifiers,
216    AlterTableAlterColumnSetUnsupportedAction,
217    AlterTableAlterColumnUnsupportedAction,
218    AlterTableAlterStatementBeyondAlterColumn,
219    AlterTableDropColumnIfExistsSyntax,
220    AlterTableDropColumnModifiers,
221    AlterTableDropStatementBeyondDropColumn,
222    AlterTableRenameColumnMissingTo,
223    AlterTableRenameColumnModifiers,
224    AlterTableRenameStatementBeyondRenameColumn,
225    AlterTableUnsupportedOperation,
226    ColumnAlias,
227    CreateIndexIfNotExistsSyntax,
228    CreateIndexKeyOrderingModifiers,
229    CreateIndexModifiers,
230    CreateStatementBeyondCreateIndex,
231    DescribeModifier,
232    DdlSchemaVersionDuplicateExpectedClause,
233    DdlSchemaVersionDuplicateSetClause,
234    DropIndexModifiers,
235    DropIndexIfExistsSyntax,
236    DropStatementBeyondDropIndex,
237    ExpressionIndexUnsupportedFunction,
238    Having,
239    Insert,
240    Join,
241    LikePatternBeyondTrailingPrefix,
242    LowerFieldPredicateUnsupported,
243    MultiStatementSql,
244    NestedAggregateInput,
245    NestedProjectionFunctionInArithmetic,
246    OrderByUnsupportedForm,
247    Other,
248    ParameterBinding,
249    ParameterizedSchemaVersion,
250    PredicateStartsWithFirstArgument,
251    QuotedIdentifiers,
252    ReturningUnsupportedShape,
253    ScalarFunctionExpressionPosition,
254    ScaleTakingNumericFunctionExpressionPosition,
255    SearchedCaseGroupedOrderBy,
256    ShowColumnsModifiers,
257    ShowEntitiesModifiers,
258    ShowIndexesModifiers,
259    ShowMemoryModifiers,
260    ShowStoresModifiers,
261    ShowUnsupportedCommand,
262    SimpleCaseExpression,
263    StandaloneLiteralProjectionItem,
264    SupportedGroupedOrderByExpressionFamily,
265    SupportedOrderByExpressionFamily,
266    UnionIntersectExcept,
267    UnsupportedFunctionNamespace,
268    Update,
269    UpperFieldPredicateUnsupported,
270    WindowFunction,
271    With,
272}
273
274///
275/// SqlSurfaceMismatchCode
276///
277/// Compact SQL endpoint surface mismatch identifier.
278///
279
280#[cfg_attr(
281    feature = "wire",
282    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
283)]
284#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
285pub enum SqlSurfaceMismatchCode {
286    QueryRejectsInsert,
287    QueryRejectsUpdate,
288    QueryRejectsDelete,
289    UpdateRejectsSelect,
290    UpdateRejectsExplain,
291    UpdateRejectsDescribe,
292    UpdateRejectsShowIndexes,
293    UpdateRejectsShowColumns,
294    UpdateRejectsShowEntities,
295    UpdateRejectsShowStores,
296    UpdateRejectsShowMemory,
297}
298
299///
300/// SchemaDdlAdmissionCode
301///
302/// Compact SQL DDL admission rejection reason.
303///
304
305#[cfg_attr(
306    feature = "wire",
307    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
308)]
309#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
310pub enum SchemaDdlAdmissionCode {
311    MissingExpectedSchemaVersion,
312    MissingNextSchemaVersion,
313    StaleExpectedSchemaVersion,
314    InvalidExpectedSchemaVersion,
315    InvalidNextSchemaVersion,
316    AcceptedSchemaChangeWithoutVersionBump,
317    EmptyVersionBump,
318    VersionGap,
319    VersionRollback,
320    FingerprintMethodMismatch,
321    UnsupportedTransitionClass,
322    PhysicalRunnerMissing,
323    ValidationFailed,
324    PublicationRaceLost,
325}
326
327///
328/// DiagnosticDetail
329///
330/// Small structured diagnostic payload for callers and CLI rendering.
331///
332
333#[cfg_attr(
334    feature = "wire",
335    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
336)]
337#[derive(Clone, Debug, Eq, PartialEq)]
338pub enum DiagnosticDetail {
339    QueryKind { kind: QueryErrorKind },
340    RuntimeKind { kind: RuntimeErrorKind },
341    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
342    UnsupportedSqlFeature { feature: SqlFeatureCode },
343    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
344}
345
346///
347/// Diagnostic
348///
349/// Compact public diagnostic payload.
350///
351
352#[cfg_attr(
353    feature = "wire",
354    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
355)]
356#[derive(Clone, Debug, Eq, PartialEq)]
357pub struct Diagnostic {
358    code: DiagnosticCode,
359    origin: ErrorOrigin,
360    detail: Option<DiagnosticDetail>,
361}
362
363impl Diagnostic {
364    /// Build a compact diagnostic from a code and optional structured detail.
365    #[must_use]
366    pub const fn new(
367        code: DiagnosticCode,
368        origin: ErrorOrigin,
369        detail: Option<DiagnosticDetail>,
370    ) -> Self {
371        Self {
372            code,
373            origin,
374            detail,
375        }
376    }
377
378    /// Build a compact diagnostic using the code's default origin.
379    #[must_use]
380    pub const fn from_code(code: DiagnosticCode) -> Self {
381        Self::new(code, code.origin(), None)
382    }
383
384    /// Return the stable diagnostic code.
385    #[must_use]
386    pub const fn code(&self) -> DiagnosticCode {
387        self.code
388    }
389
390    /// Return the diagnostic class.
391    #[must_use]
392    pub const fn class(&self) -> ErrorClass {
393        self.code.class()
394    }
395
396    /// Return the subsystem origin.
397    #[must_use]
398    pub const fn origin(&self) -> ErrorOrigin {
399        self.origin
400    }
401
402    /// Return structured diagnostic detail, when available.
403    #[must_use]
404    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
405        self.detail.as_ref()
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::{Diagnostic, DiagnosticCode, ErrorClass, ErrorOrigin};
412
413    #[test]
414    fn diagnostic_from_code_uses_default_origin() {
415        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
416
417        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
418        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
419    }
420
421    #[test]
422    fn diagnostic_code_reports_broad_class() {
423        assert_eq!(
424            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
425            ErrorClass::Unsupported
426        );
427        assert_eq!(
428            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
429            ErrorClass::Unsupported
430        );
431        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
432        assert_eq!(
433            DiagnosticCode::StoreCorruption.class(),
434            ErrorClass::Corruption
435        );
436    }
437}