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/// RuntimeBoundaryCode
199///
200/// Compact public-runtime boundary identifier.
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 RuntimeBoundaryCode {
209    SqlSurfaceControllerRequired,
210    SchemaSurfaceControllerRequired,
211    SqlQueryNoConfiguredEntities,
212    SqlQueryEntityNotConfigured,
213    SqlDdlTargetRequired,
214    SqlDdlEntityNotConfigured,
215    QueryResponseRowsRequired,
216    QueryResponseGroupedRowsRequired,
217    MutationResultEntityRequired,
218    MutationResultEntitiesRequired,
219    MutationResultIdRequired,
220    MutationResultIdsRequired,
221    RowProjectionFieldNotConfigured,
222}
223
224///
225/// SqlFeatureCode
226///
227/// Compact SQL feature identifier used by unsupported-feature diagnostics.
228///
229
230#[cfg_attr(
231    feature = "wire",
232    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
233)]
234#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
235pub enum SqlFeatureCode {
236    AggregateFilterClause,
237    AlterStatementBeyondAlterTable,
238    AlterTableAddColumnDuplicateDefault,
239    AlterTableAddColumnModifiers,
240    AlterTableAddStatementBeyondAddColumn,
241    AlterTableAlterColumnDropUnsupportedAction,
242    AlterTableAlterColumnModifiers,
243    AlterTableAlterColumnSetUnsupportedAction,
244    AlterTableAlterColumnUnsupportedAction,
245    AlterTableAlterStatementBeyondAlterColumn,
246    AlterTableDropColumnIfExistsSyntax,
247    AlterTableDropColumnModifiers,
248    AlterTableDropStatementBeyondDropColumn,
249    AlterTableRenameColumnMissingTo,
250    AlterTableRenameColumnModifiers,
251    AlterTableRenameStatementBeyondRenameColumn,
252    AlterTableUnsupportedOperation,
253    ColumnAlias,
254    CreateIndexIfNotExistsSyntax,
255    CreateIndexKeyOrderingModifiers,
256    CreateIndexModifiers,
257    CreateStatementBeyondCreateIndex,
258    DescribeModifier,
259    DdlSchemaVersionDuplicateExpectedClause,
260    DdlSchemaVersionDuplicateSetClause,
261    DropIndexModifiers,
262    DropIndexIfExistsSyntax,
263    DropStatementBeyondDropIndex,
264    ExpressionIndexUnsupportedFunction,
265    Having,
266    Insert,
267    Join,
268    LikePatternBeyondTrailingPrefix,
269    LowerFieldPredicateUnsupported,
270    MultiStatementSql,
271    NestedAggregateInput,
272    NestedProjectionFunctionInArithmetic,
273    OrderByUnsupportedForm,
274    Other,
275    ParameterBinding,
276    ParameterizedSchemaVersion,
277    PredicateStartsWithFirstArgument,
278    QuotedIdentifiers,
279    ReturningUnsupportedShape,
280    ScalarFunctionExpressionPosition,
281    ScaleTakingNumericFunctionExpressionPosition,
282    SearchedCaseGroupedOrderBy,
283    ShowColumnsModifiers,
284    ShowEntitiesModifiers,
285    ShowIndexesModifiers,
286    ShowMemoryModifiers,
287    ShowStoresModifiers,
288    ShowUnsupportedCommand,
289    SimpleCaseExpression,
290    StandaloneLiteralProjectionItem,
291    SupportedGroupedOrderByExpressionFamily,
292    SupportedOrderByExpressionFamily,
293    UnionIntersectExcept,
294    UnsupportedFunctionNamespace,
295    Update,
296    UpperFieldPredicateUnsupported,
297    WindowFunction,
298    With,
299}
300
301///
302/// SqlSurfaceMismatchCode
303///
304/// Compact SQL endpoint surface mismatch identifier.
305///
306
307#[cfg_attr(
308    feature = "wire",
309    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
310)]
311#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
312pub enum SqlSurfaceMismatchCode {
313    QueryRejectsInsert,
314    QueryRejectsUpdate,
315    QueryRejectsDelete,
316    UpdateRejectsSelect,
317    UpdateRejectsExplain,
318    UpdateRejectsDescribe,
319    UpdateRejectsShowIndexes,
320    UpdateRejectsShowColumns,
321    UpdateRejectsShowEntities,
322    UpdateRejectsShowStores,
323    UpdateRejectsShowMemory,
324}
325
326///
327/// SchemaDdlAdmissionCode
328///
329/// Compact SQL DDL admission rejection reason.
330///
331
332#[cfg_attr(
333    feature = "wire",
334    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
335)]
336#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
337pub enum SchemaDdlAdmissionCode {
338    MissingExpectedSchemaVersion,
339    MissingNextSchemaVersion,
340    StaleExpectedSchemaVersion,
341    InvalidExpectedSchemaVersion,
342    InvalidNextSchemaVersion,
343    AcceptedSchemaChangeWithoutVersionBump,
344    EmptyVersionBump,
345    VersionGap,
346    VersionRollback,
347    FingerprintMethodMismatch,
348    UnsupportedTransitionClass,
349    PhysicalRunnerMissing,
350    ValidationFailed,
351    PublicationRaceLost,
352}
353
354///
355/// DiagnosticDetail
356///
357/// Small structured diagnostic payload for callers and CLI rendering.
358///
359
360#[cfg_attr(
361    feature = "wire",
362    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
363)]
364#[derive(Clone, Debug, Eq, PartialEq)]
365pub enum DiagnosticDetail {
366    QueryKind { kind: QueryErrorKind },
367    RuntimeKind { kind: RuntimeErrorKind },
368    RuntimeBoundary { boundary: RuntimeBoundaryCode },
369    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
370    UnsupportedSqlFeature { feature: SqlFeatureCode },
371    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
372}
373
374///
375/// Diagnostic
376///
377/// Compact public diagnostic payload.
378///
379
380#[cfg_attr(
381    feature = "wire",
382    derive(candid::CandidType, serde::Deserialize, serde::Serialize)
383)]
384#[derive(Clone, Debug, Eq, PartialEq)]
385pub struct Diagnostic {
386    code: DiagnosticCode,
387    origin: ErrorOrigin,
388    detail: Option<DiagnosticDetail>,
389}
390
391impl Diagnostic {
392    /// Build a compact diagnostic from a code and optional structured detail.
393    #[must_use]
394    pub const fn new(
395        code: DiagnosticCode,
396        origin: ErrorOrigin,
397        detail: Option<DiagnosticDetail>,
398    ) -> Self {
399        Self {
400            code,
401            origin,
402            detail,
403        }
404    }
405
406    /// Build a compact diagnostic using the code's default origin.
407    #[must_use]
408    pub const fn from_code(code: DiagnosticCode) -> Self {
409        Self::new(code, code.origin(), None)
410    }
411
412    /// Return the stable diagnostic code.
413    #[must_use]
414    pub const fn code(&self) -> DiagnosticCode {
415        self.code
416    }
417
418    /// Return the diagnostic class.
419    #[must_use]
420    pub const fn class(&self) -> ErrorClass {
421        self.code.class()
422    }
423
424    /// Return the subsystem origin.
425    #[must_use]
426    pub const fn origin(&self) -> ErrorOrigin {
427        self.origin
428    }
429
430    /// Return structured diagnostic detail, when available.
431    #[must_use]
432    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
433        self.detail.as_ref()
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::{Diagnostic, DiagnosticCode, ErrorClass, ErrorOrigin};
440
441    #[test]
442    fn diagnostic_from_code_uses_default_origin() {
443        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
444
445        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
446        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
447    }
448
449    #[test]
450    fn diagnostic_code_reports_broad_class() {
451        assert_eq!(
452            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
453            ErrorClass::Unsupported
454        );
455        assert_eq!(
456            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
457            ErrorClass::Unsupported
458        );
459        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
460        assert_eq!(
461            DiagnosticCode::StoreCorruption.class(),
462            ErrorClass::Corruption
463        );
464    }
465}