Skip to main content

icydb_diagnostic_code/
lib.rs

1//! Module: lib
2//! Responsibility: compact diagnostic identity and public numeric error-code mapping.
3//! Does not own: rich diagnostic prose, Candid wire types, or runtime error construction.
4//! Boundary: maps rich internal diagnostic categories to stable compact public codes.
5//!
6//! This crate intentionally contains no rich diagnostic prose or Candid wire
7//! types. Production canister builds collapse diagnostics to numeric wire
8//! codes before they cross the public canister boundary. `Debug` output is
9//! numeric for the same reason: host tooling can recover labels from the code
10//! table without making every wasm canister retain those labels.
11
12use std::fmt;
13
14///
15/// DiagnosticCode
16///
17/// Stable machine-readable diagnostic reason.
18///
19
20#[remain::sorted]
21#[derive(Clone, Copy, Eq, Hash, PartialEq)]
22pub enum DiagnosticCode {
23    QueryAccessRequirement,
24    QueryIntent,
25    QueryInvalidContinuationCursor,
26    QueryNotFound,
27    QueryNotUnique,
28    QueryNumericNotRepresentable,
29    QueryNumericOverflow,
30    QueryPlan,
31    QueryResultShapeMismatch,
32    QuerySqlSurfaceMismatch,
33    QuerySqlWriteBoundary,
34    QueryUnknownAggregateTargetField,
35    QueryUnorderedPagination,
36    QueryUnsupportedProjection,
37    QueryUnsupportedSqlFeature,
38    QueryValidate,
39    RuntimeConflict,
40    RuntimeCorruption,
41    RuntimeIncompatiblePersistedFormat,
42    RuntimeInternal,
43    RuntimeInvariantViolation,
44    RuntimeNotFound,
45    RuntimeUnsupported,
46    SchemaDdlAdmission,
47    StoreCorruption,
48    StoreInvariantViolation,
49    StoreNotFound,
50}
51
52impl DiagnosticCode {
53    /// Return the broad diagnostic class for this code.
54    #[must_use]
55    pub const fn class(self) -> ErrorClass {
56        match self {
57            Self::StoreCorruption | Self::RuntimeCorruption => ErrorClass::Corruption,
58            Self::RuntimeIncompatiblePersistedFormat => ErrorClass::IncompatiblePersistedFormat,
59            Self::QueryNotFound | Self::StoreNotFound | Self::RuntimeNotFound => {
60                ErrorClass::NotFound
61            }
62            Self::RuntimeConflict => ErrorClass::Conflict,
63            Self::QueryUnsupportedSqlFeature
64            | Self::QueryUnknownAggregateTargetField
65            | Self::QueryUnsupportedProjection
66            | Self::QueryResultShapeMismatch
67            | Self::QuerySqlSurfaceMismatch
68            | Self::QuerySqlWriteBoundary
69            | Self::RuntimeUnsupported => ErrorClass::Unsupported,
70            Self::StoreInvariantViolation | Self::RuntimeInvariantViolation => {
71                ErrorClass::InvariantViolation
72            }
73            Self::RuntimeInternal => ErrorClass::Internal,
74            Self::QueryValidate
75            | Self::QueryIntent
76            | Self::QueryPlan
77            | Self::QueryAccessRequirement
78            | Self::QueryUnorderedPagination
79            | Self::QueryInvalidContinuationCursor
80            | Self::QueryNotUnique
81            | Self::QueryNumericOverflow
82            | Self::QueryNumericNotRepresentable
83            | Self::SchemaDdlAdmission => ErrorClass::Query,
84        }
85    }
86
87    /// Return the default diagnostic origin for this code.
88    #[must_use]
89    pub const fn origin(self) -> ErrorOrigin {
90        match self {
91            Self::StoreNotFound | Self::StoreCorruption | Self::StoreInvariantViolation => {
92                ErrorOrigin::Store
93            }
94            Self::RuntimeCorruption
95            | Self::RuntimeIncompatiblePersistedFormat
96            | Self::RuntimeInvariantViolation
97            | Self::RuntimeConflict
98            | Self::RuntimeNotFound
99            | Self::RuntimeUnsupported
100            | Self::RuntimeInternal => ErrorOrigin::Runtime,
101            Self::QueryValidate
102            | Self::QueryIntent
103            | Self::QueryPlan
104            | Self::QueryAccessRequirement
105            | Self::QueryUnorderedPagination
106            | Self::QueryInvalidContinuationCursor
107            | Self::QueryNotFound
108            | Self::QueryNotUnique
109            | Self::QueryNumericOverflow
110            | Self::QueryNumericNotRepresentable
111            | Self::QueryUnknownAggregateTargetField
112            | Self::QueryUnsupportedProjection
113            | Self::QueryResultShapeMismatch
114            | Self::QueryUnsupportedSqlFeature
115            | Self::QuerySqlSurfaceMismatch
116            | Self::QuerySqlWriteBoundary
117            | Self::SchemaDdlAdmission => ErrorOrigin::Query,
118        }
119    }
120
121    /// Return the compact public wire code for this broad diagnostic reason.
122    #[must_use]
123    pub const fn error_code(self) -> ErrorCode {
124        match self {
125            Self::QueryValidate => ErrorCode::QUERY_VALIDATE,
126            Self::QueryIntent => ErrorCode::QUERY_INTENT,
127            Self::QueryPlan => ErrorCode::QUERY_PLAN,
128            Self::QueryAccessRequirement => ErrorCode::QUERY_ACCESS_REQUIREMENT,
129            Self::QueryUnorderedPagination => ErrorCode::QUERY_UNORDERED_PAGINATION,
130            Self::QueryInvalidContinuationCursor => ErrorCode::QUERY_INVALID_CONTINUATION_CURSOR,
131            Self::QueryNotFound => ErrorCode::QUERY_NOT_FOUND,
132            Self::QueryNotUnique => ErrorCode::QUERY_NOT_UNIQUE,
133            Self::QueryNumericOverflow => ErrorCode::QUERY_NUMERIC_OVERFLOW,
134            Self::QueryNumericNotRepresentable => ErrorCode::QUERY_NUMERIC_NOT_REPRESENTABLE,
135            Self::QueryUnknownAggregateTargetField => {
136                ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD
137            }
138            Self::QueryUnsupportedProjection => ErrorCode::QUERY_UNSUPPORTED_PROJECTION,
139            Self::QueryResultShapeMismatch => ErrorCode::QUERY_RESULT_SHAPE_MISMATCH,
140            Self::QueryUnsupportedSqlFeature => ErrorCode::QUERY_UNSUPPORTED_SQL_FEATURE,
141            Self::QuerySqlSurfaceMismatch => ErrorCode::QUERY_SQL_SURFACE_MISMATCH,
142            Self::QuerySqlWriteBoundary => ErrorCode::QUERY_SQL_WRITE_BOUNDARY,
143            Self::SchemaDdlAdmission => ErrorCode::SCHEMA_DDL_ADMISSION,
144            Self::StoreNotFound => ErrorCode::STORE_NOT_FOUND,
145            Self::StoreCorruption => ErrorCode::STORE_CORRUPTION,
146            Self::StoreInvariantViolation => ErrorCode::STORE_INVARIANT_VIOLATION,
147            Self::RuntimeCorruption => ErrorCode::RUNTIME_CORRUPTION,
148            Self::RuntimeIncompatiblePersistedFormat => {
149                ErrorCode::RUNTIME_INCOMPATIBLE_PERSISTED_FORMAT
150            }
151            Self::RuntimeInvariantViolation => ErrorCode::RUNTIME_INVARIANT_VIOLATION,
152            Self::RuntimeConflict => ErrorCode::RUNTIME_CONFLICT,
153            Self::RuntimeNotFound => ErrorCode::RUNTIME_NOT_FOUND,
154            Self::RuntimeUnsupported => ErrorCode::RUNTIME_UNSUPPORTED,
155            Self::RuntimeInternal => ErrorCode::RUNTIME_INTERNAL,
156        }
157    }
158}
159
160impl fmt::Debug for DiagnosticCode {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        fmt_compact_code(f, self.error_code().raw())
163    }
164}
165
166///
167/// ErrorCode
168///
169/// Stable numeric public error identity.
170///
171/// The public Candid `icydb::Error` stores this value as `nat16` so canister
172/// interfaces do not retain rich diagnostic enum labels. Rich diagnostics can
173/// still be reconstructed by host-side tooling from this leaf code. Before
174/// 1.0.0, the code space is hard-cut to a single compact sequential range.
175///
176
177#[derive(Clone, Copy, Eq, Hash, PartialEq)]
178pub struct ErrorCode(u16);
179
180mod registry;
181
182impl ErrorCode {
183    /// Build an error code from its raw public wire value.
184    #[must_use]
185    pub const fn from_raw(raw: u16) -> Self {
186        Self(raw)
187    }
188
189    /// Return the raw public wire value.
190    #[must_use]
191    pub const fn raw(self) -> u16 {
192        self.0
193    }
194}
195
196impl fmt::Debug for ErrorCode {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        fmt_compact_code(f, self.raw())
199    }
200}
201
202///
203/// ErrorClass
204///
205/// Broad diagnostic class used for recovery decisions.
206///
207
208#[remain::sorted]
209#[derive(Clone, Copy, Eq, Hash, PartialEq)]
210pub enum ErrorClass {
211    Conflict,
212    Corruption,
213    IncompatiblePersistedFormat,
214    Internal,
215    InvariantViolation,
216    NotFound,
217    Query,
218    Unsupported,
219}
220
221impl ErrorClass {
222    /// Return the compact public wire code for this diagnostic class.
223    #[must_use]
224    pub const fn wire_code(self) -> u8 {
225        match self {
226            Self::Query => 1,
227            Self::Corruption => 2,
228            Self::IncompatiblePersistedFormat => 3,
229            Self::NotFound => 4,
230            Self::Internal => 5,
231            Self::Conflict => 6,
232            Self::Unsupported => 7,
233            Self::InvariantViolation => 8,
234        }
235    }
236
237    /// Recover a diagnostic class from its compact public wire code.
238    #[must_use]
239    pub const fn from_wire_code(code: u8) -> Option<Self> {
240        match code {
241            1 => Some(Self::Query),
242            2 => Some(Self::Corruption),
243            3 => Some(Self::IncompatiblePersistedFormat),
244            4 => Some(Self::NotFound),
245            5 => Some(Self::Internal),
246            6 => Some(Self::Conflict),
247            7 => Some(Self::Unsupported),
248            8 => Some(Self::InvariantViolation),
249            _ => None,
250        }
251    }
252}
253
254impl fmt::Debug for ErrorClass {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        fmt_compact_code(f, u16::from(self.wire_code()))
257    }
258}
259
260///
261/// ErrorOrigin
262///
263/// Subsystem that owns the diagnostic.
264///
265
266#[remain::sorted]
267#[derive(Clone, Copy, Eq, Hash, PartialEq)]
268pub enum ErrorOrigin {
269    Cursor,
270    Executor,
271    Identity,
272    Index,
273    Interface,
274    Planner,
275    Query,
276    Recovery,
277    Response,
278    Runtime,
279    Serialize,
280    Store,
281}
282
283impl ErrorOrigin {
284    /// Return the compact public wire code for this diagnostic origin.
285    #[must_use]
286    pub const fn wire_code(self) -> u8 {
287        match self {
288            Self::Cursor => 1,
289            Self::Executor => 2,
290            Self::Identity => 3,
291            Self::Index => 4,
292            Self::Interface => 5,
293            Self::Planner => 6,
294            Self::Query => 7,
295            Self::Recovery => 8,
296            Self::Response => 9,
297            Self::Runtime => 10,
298            Self::Serialize => 11,
299            Self::Store => 12,
300        }
301    }
302
303    /// Recover a known diagnostic origin from its compact public wire code.
304    #[must_use]
305    pub const fn from_known_wire_code(code: u8) -> Option<Self> {
306        match code {
307            1 => Some(Self::Cursor),
308            2 => Some(Self::Executor),
309            3 => Some(Self::Identity),
310            4 => Some(Self::Index),
311            5 => Some(Self::Interface),
312            6 => Some(Self::Planner),
313            7 => Some(Self::Query),
314            8 => Some(Self::Recovery),
315            9 => Some(Self::Response),
316            10 => Some(Self::Runtime),
317            11 => Some(Self::Serialize),
318            12 => Some(Self::Store),
319            _ => None,
320        }
321    }
322
323    /// Recover a diagnostic origin from its compact public wire code.
324    ///
325    /// Unknown origin codes fail closed to `Runtime`, matching the public
326    /// boundary behavior used by the Candid facade.
327    #[must_use]
328    pub const fn from_wire_code(code: u8) -> Self {
329        match Self::from_known_wire_code(code) {
330            Some(origin) => origin,
331            None => Self::Runtime,
332        }
333    }
334}
335
336impl fmt::Debug for ErrorOrigin {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        fmt_compact_code(f, u16::from(self.wire_code()))
339    }
340}
341
342///
343/// QueryErrorKind
344///
345/// Public query error category.
346///
347
348#[repr(u16)]
349#[derive(Clone, Copy, Eq, Hash, PartialEq)]
350pub enum QueryErrorKind {
351    Validate,
352    Intent,
353    Plan,
354    AccessRequirement,
355    UnorderedPagination,
356    InvalidContinuationCursor,
357    NotFound,
358    NotUnique,
359}
360
361impl fmt::Debug for QueryErrorKind {
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        fmt_compact_code(f, *self as u16)
364    }
365}
366
367///
368/// QueryProjectionCode
369///
370/// Compact query projection admission/runtime identifier.
371/// Variant order is wire-order significant for public error-code offsets.
372///
373
374#[repr(u16)]
375#[derive(Clone, Copy, Eq, Hash, PartialEq)]
376pub enum QueryProjectionCode {
377    NumericLiteralRequired,
378    NumericScaleArguments,
379    NestedFieldPathPreview,
380    CaseConditionBooleanRequired,
381    NumericInputRequired,
382    TextOrBlobInputRequired,
383    TextInputRequired,
384    TextOrNullArgumentRequired,
385    IntegerOrNullArgumentRequired,
386    UnaryOperandIncompatible,
387    BinaryOperandsIncompatible,
388}
389
390impl fmt::Debug for QueryProjectionCode {
391    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392        fmt_compact_code(f, *self as u16)
393    }
394}
395
396///
397/// QueryResultShapeCode
398///
399/// Compact query-result shape mismatch identifier.
400/// Variant order is wire-order significant for public error-code offsets.
401///
402
403#[repr(u16)]
404#[derive(Clone, Copy, Eq, Hash, PartialEq)]
405pub enum QueryResultShapeCode {
406    ExpectedRows,
407    ExpectedGroupedRows,
408}
409
410impl fmt::Debug for QueryResultShapeCode {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        fmt_compact_code(f, *self as u16)
413    }
414}
415
416///
417/// RuntimeErrorKind
418///
419/// Public runtime error category.
420///
421
422#[repr(u16)]
423#[derive(Clone, Copy, Eq, Hash, PartialEq)]
424pub enum RuntimeErrorKind {
425    Corruption,
426    IncompatiblePersistedFormat,
427    InvariantViolation,
428    Conflict,
429    NotFound,
430    Unsupported,
431    Internal,
432}
433
434impl fmt::Debug for RuntimeErrorKind {
435    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436        fmt_compact_code(f, *self as u16)
437    }
438}
439
440///
441/// RuntimeBoundaryCode
442///
443/// Compact public-runtime boundary identifier.
444/// Variant order is wire-order significant for public error-code offsets.
445///
446
447#[repr(u16)]
448#[derive(Clone, Copy, Eq, Hash, PartialEq)]
449pub enum RuntimeBoundaryCode {
450    SqlSurfaceControllerRequired,
451    SchemaSurfaceControllerRequired,
452    SqlQueryNoConfiguredEntities,
453    SqlQueryEntityNotConfigured,
454    SqlDdlTargetRequired,
455    SqlDdlEntityNotConfigured,
456    QueryResponseRowsRequired,
457    QueryResponseGroupedRowsRequired,
458    MutationResultEntityRequired,
459    MutationResultEntitiesRequired,
460    MutationResultIdRequired,
461    MutationResultIdsRequired,
462    RowProjectionFieldNotConfigured,
463    SqlIntrospectionDisabled,
464}
465
466impl fmt::Debug for RuntimeBoundaryCode {
467    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468        fmt_compact_code(f, *self as u16)
469    }
470}
471
472///
473/// SqlFeatureCode
474///
475/// Compact SQL feature identifier used by unsupported-feature diagnostics.
476/// Variant order is wire-order significant for public error-code offsets.
477///
478
479#[repr(u16)]
480#[derive(Clone, Copy, Eq, Hash, PartialEq)]
481pub enum SqlFeatureCode {
482    AggregateFilterClause,
483    AlterStatementBeyondAlterTable,
484    AlterTableAddColumnDuplicateDefault,
485    AlterTableAddColumnModifiers,
486    AlterTableAddStatementBeyondAddColumn,
487    AlterTableAlterColumnDropUnsupportedAction,
488    AlterTableAlterColumnModifiers,
489    AlterTableAlterColumnSetUnsupportedAction,
490    AlterTableAlterColumnUnsupportedAction,
491    AlterTableAlterStatementBeyondAlterColumn,
492    AlterTableDropColumnIfExistsSyntax,
493    AlterTableDropColumnModifiers,
494    AlterTableDropStatementBeyondDropColumn,
495    AlterTableRenameColumnMissingTo,
496    AlterTableRenameColumnModifiers,
497    AlterTableRenameStatementBeyondRenameColumn,
498    AlterTableUnsupportedOperation,
499    ColumnAlias,
500    CreateIndexIfNotExistsSyntax,
501    CreateIndexKeyOrderingModifiers,
502    CreateIndexModifiers,
503    CreateStatementBeyondCreateIndex,
504    DescribeModifier,
505    DdlSchemaVersionDuplicateExpectedClause,
506    DdlSchemaVersionDuplicateSetClause,
507    DropIndexModifiers,
508    DropIndexIfExistsSyntax,
509    DropStatementBeyondDropIndex,
510    ExpressionIndexUnsupportedFunction,
511    Having,
512    Insert,
513    Join,
514    LikePatternBeyondTrailingPrefix,
515    LowerFieldPredicateUnsupported,
516    MultiStatementSql,
517    NestedAggregateInput,
518    NestedProjectionFunctionInArithmetic,
519    OrderByUnsupportedForm,
520    Other,
521    ParameterBinding,
522    ParameterizedSchemaVersion,
523    PredicateStartsWithFirstArgument,
524    QuotedIdentifiers,
525    ReturningUnsupportedShape,
526    ScalarFunctionExpressionPosition,
527    ScaleTakingNumericFunctionExpressionPosition,
528    SearchedCaseGroupedOrderBy,
529    ShowColumnsModifiers,
530    ShowEntitiesModifiers,
531    ShowIndexesModifiers,
532    ShowMemoryModifiers,
533    ShowStoresModifiers,
534    ShowUnsupportedCommand,
535    SimpleCaseExpression,
536    StandaloneLiteralProjectionItem,
537    SupportedGroupedOrderByExpressionFamily,
538    SupportedOrderByExpressionFamily,
539    UnionIntersectExcept,
540    UnsupportedFunctionNamespace,
541    Update,
542    UpperFieldPredicateUnsupported,
543    WindowFunction,
544    With,
545    NumericScaleFunctionArguments,
546    OrderByFieldNotOrderable,
547}
548
549impl fmt::Debug for SqlFeatureCode {
550    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
551        fmt_compact_code(f, *self as u16)
552    }
553}
554
555///
556/// SqlLoweringCode
557///
558/// Compact SQL lowering rejection identifier used after parsing succeeds but
559/// before a statement becomes canonical query intent.
560/// Variant order is wire-order significant for public error-code offsets.
561///
562
563#[repr(u16)]
564#[derive(Clone, Copy, Eq, Hash, PartialEq)]
565pub enum SqlLoweringCode {
566    EntityMismatch,
567    SelectProjectionShape,
568    SelectDistinct,
569    DistinctOrderByProjection,
570    GlobalAggregateProjection,
571    GlobalAggregateGroupBy,
572    SelectGroupByShape,
573    GroupedProjectionExplicitListRequired,
574    GroupedProjectionAggregateRequired,
575    GroupedProjectionNonGroupField,
576    GroupedProjectionScalarAfterAggregate,
577    HavingRequiresGroupBy,
578    SelectHavingShape,
579    AggregateInputExpressions,
580    WhereExpressionShape,
581    ParameterPlacement,
582    SqlDdlExecutionUnsupported,
583}
584
585impl fmt::Debug for SqlLoweringCode {
586    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587        fmt_compact_code(f, *self as u16)
588    }
589}
590
591///
592/// SqlSurfaceMismatchCode
593///
594/// Compact SQL endpoint surface mismatch identifier.
595/// Variant order is wire-order significant for public error-code offsets.
596///
597
598#[repr(u16)]
599#[derive(Clone, Copy, Eq, Hash, PartialEq)]
600pub enum SqlSurfaceMismatchCode {
601    QueryRejectsInsert,
602    QueryRejectsUpdate,
603    QueryRejectsDelete,
604    UpdateRejectsSelect,
605    UpdateRejectsExplain,
606    UpdateRejectsDescribe,
607    UpdateRejectsShowIndexes,
608    UpdateRejectsShowColumns,
609    UpdateRejectsShowEntities,
610    UpdateRejectsShowStores,
611    UpdateRejectsShowMemory,
612}
613
614impl fmt::Debug for SqlSurfaceMismatchCode {
615    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616        fmt_compact_code(f, *self as u16)
617    }
618}
619
620///
621/// SqlWriteBoundaryCode
622///
623/// Compact SQL write fail-closed boundary identifier.
624/// Variant order is wire-order significant for public error-code offsets.
625///
626
627#[repr(u16)]
628#[derive(Clone, Copy, Eq, Hash, PartialEq)]
629pub enum SqlWriteBoundaryCode {
630    PrimaryKeyLiteralShape,
631    PrimaryKeyLiteralIncompatible,
632    MissingPrimaryKey,
633    MissingRequiredFields,
634    ExplicitManagedField,
635    ExplicitGeneratedField,
636    InsertSelectRequiresScalar,
637    InsertSelectAggregateProjection,
638    InsertSelectWidthMismatch,
639    UpdatePrimaryKeyMutation,
640    InvalidFieldLiteral,
641    UnknownReturningField,
642    DuplicateReturningField,
643    UpdateMissingWherePredicate,
644    WriteOrderByUnsupportedShape,
645    ReturningResponseTooLarge,
646    ReturningRowsTooMany,
647}
648
649impl fmt::Debug for SqlWriteBoundaryCode {
650    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
651        fmt_compact_code(f, *self as u16)
652    }
653}
654
655///
656/// SchemaDdlAdmissionCode
657///
658/// Compact SQL DDL admission rejection reason.
659/// Variant order is wire-order significant for public error-code offsets.
660///
661
662#[repr(u16)]
663#[derive(Clone, Copy, Eq, Hash, PartialEq)]
664pub enum SchemaDdlAdmissionCode {
665    MissingExpectedSchemaVersion,
666    MissingNextSchemaVersion,
667    StaleExpectedSchemaVersion,
668    InvalidExpectedSchemaVersion,
669    InvalidNextSchemaVersion,
670    AcceptedSchemaChangeWithoutVersionBump,
671    EmptyVersionBump,
672    VersionGap,
673    VersionRollback,
674    FingerprintMethodMismatch,
675    UnsupportedTransitionClass,
676    PhysicalRunnerMissing,
677    ValidationFailed,
678    PublicationRaceLost,
679    InvalidAddColumnDefault,
680    InvalidAlterColumnDefault,
681    GeneratedIndexDropRejected,
682    RequiredDropDefaultUnsupported,
683    GeneratedFieldDefaultChangeRejected,
684    GeneratedFieldNullabilityChangeRejected,
685    SetNotNullValidationFailed,
686}
687
688impl fmt::Debug for SchemaDdlAdmissionCode {
689    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690        fmt_compact_code(f, *self as u16)
691    }
692}
693
694///
695/// DiagnosticDetail
696///
697/// Small structured diagnostic payload for callers and CLI rendering.
698///
699
700#[remain::sorted]
701#[derive(Clone, Copy, Eq, PartialEq)]
702pub enum DiagnosticDetail {
703    QueryKind { kind: QueryErrorKind },
704    QueryProjection { reason: QueryProjectionCode },
705    QueryResultShape { reason: QueryResultShapeCode },
706    RuntimeBoundary { boundary: RuntimeBoundaryCode },
707    RuntimeKind { kind: RuntimeErrorKind },
708    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
709    SqlLowering { reason: SqlLoweringCode },
710    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
711    SqlWriteBoundary { boundary: SqlWriteBoundaryCode },
712    UnsupportedSqlFeature { feature: SqlFeatureCode },
713}
714
715impl fmt::Debug for DiagnosticDetail {
716    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
717        fmt_compact_code(
718            f,
719            ErrorCode::from_parts(self.diagnostic_code(), Some(*self)).raw(),
720        )
721    }
722}
723
724///
725/// Diagnostic
726///
727/// Compact public diagnostic payload.
728///
729
730#[derive(Clone, Eq, PartialEq)]
731pub struct Diagnostic {
732    code: DiagnosticCode,
733    origin: ErrorOrigin,
734    detail: Option<DiagnosticDetail>,
735}
736
737impl Diagnostic {
738    /// Build a compact diagnostic from a code and optional structured detail.
739    #[must_use]
740    pub const fn new(
741        code: DiagnosticCode,
742        origin: ErrorOrigin,
743        detail: Option<DiagnosticDetail>,
744    ) -> Self {
745        Self {
746            code,
747            origin,
748            detail,
749        }
750    }
751
752    /// Build a compact diagnostic using the code's default origin.
753    #[must_use]
754    pub const fn from_code(code: DiagnosticCode) -> Self {
755        Self::new(code, code.origin(), None)
756    }
757
758    /// Return the stable diagnostic code.
759    #[must_use]
760    pub const fn code(&self) -> DiagnosticCode {
761        self.code
762    }
763
764    /// Return the diagnostic class.
765    #[must_use]
766    pub const fn class(&self) -> ErrorClass {
767        self.code.class()
768    }
769
770    /// Return the subsystem origin.
771    #[must_use]
772    pub const fn origin(&self) -> ErrorOrigin {
773        self.origin
774    }
775
776    /// Return structured diagnostic detail, when available.
777    #[must_use]
778    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
779        self.detail.as_ref()
780    }
781
782    /// Return the numeric public wire code for this diagnostic.
783    #[must_use]
784    pub const fn error_code(&self) -> ErrorCode {
785        ErrorCode::from_parts(self.code, self.detail)
786    }
787}
788
789impl fmt::Debug for Diagnostic {
790    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
791        write!(f, "{}@{}", self.error_code().raw(), self.origin.wire_code())
792    }
793}
794
795fn fmt_compact_code(f: &mut fmt::Formatter<'_>, raw: u16) -> fmt::Result {
796    write!(f, "{raw}")
797}
798
799#[cfg(test)]
800mod tests {
801    use super::{
802        Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorClass, ErrorCode, ErrorOrigin,
803        QueryProjectionCode, SqlFeatureCode, SqlLoweringCode, SqlWriteBoundaryCode,
804        registry::{DETAIL_ERROR_CODES, ORDERED_ERROR_CODES},
805    };
806
807    #[test]
808    fn diagnostic_from_code_uses_default_origin() {
809        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
810
811        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
812        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
813    }
814
815    #[test]
816    fn diagnostic_code_reports_broad_class() {
817        assert_eq!(
818            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
819            ErrorClass::Unsupported
820        );
821        assert_eq!(
822            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
823            ErrorClass::Unsupported
824        );
825        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
826        assert_eq!(
827            DiagnosticCode::StoreCorruption.class(),
828            ErrorClass::Corruption
829        );
830    }
831
832    #[test]
833    fn class_and_origin_wire_codes_round_trip() {
834        for (class, raw) in [
835            (ErrorClass::Query, 1),
836            (ErrorClass::Corruption, 2),
837            (ErrorClass::IncompatiblePersistedFormat, 3),
838            (ErrorClass::NotFound, 4),
839            (ErrorClass::Internal, 5),
840            (ErrorClass::Conflict, 6),
841            (ErrorClass::Unsupported, 7),
842            (ErrorClass::InvariantViolation, 8),
843        ] {
844            assert_eq!(class.wire_code(), raw);
845            assert_eq!(ErrorClass::from_wire_code(raw), Some(class));
846            assert_eq!(format!("{class:?}"), raw.to_string());
847        }
848
849        for (origin, raw) in [
850            (ErrorOrigin::Cursor, 1),
851            (ErrorOrigin::Executor, 2),
852            (ErrorOrigin::Identity, 3),
853            (ErrorOrigin::Index, 4),
854            (ErrorOrigin::Interface, 5),
855            (ErrorOrigin::Planner, 6),
856            (ErrorOrigin::Query, 7),
857            (ErrorOrigin::Recovery, 8),
858            (ErrorOrigin::Response, 9),
859            (ErrorOrigin::Runtime, 10),
860            (ErrorOrigin::Serialize, 11),
861            (ErrorOrigin::Store, 12),
862        ] {
863            assert_eq!(origin.wire_code(), raw);
864            assert_eq!(ErrorOrigin::from_known_wire_code(raw), Some(origin));
865            assert_eq!(ErrorOrigin::from_wire_code(raw), origin);
866            assert_eq!(format!("{origin:?}"), raw.to_string());
867        }
868
869        assert_eq!(ErrorClass::from_wire_code(0), None);
870        assert_eq!(ErrorOrigin::from_known_wire_code(0), None);
871        assert_eq!(ErrorOrigin::from_wire_code(0), ErrorOrigin::Runtime);
872    }
873
874    #[test]
875    fn public_error_codes_are_sequential() {
876        let first = ORDERED_ERROR_CODES
877            .first()
878            .expect("public error-code registry is non-empty")
879            .raw();
880
881        assert_eq!(first, 1);
882
883        for (index, code) in ORDERED_ERROR_CODES.iter().enumerate() {
884            let expected = first + u16::try_from(index).expect("test error-code index fits u16");
885            assert_eq!(code.raw(), expected);
886            assert_eq!(ErrorCode::known(code.raw()), Some(*code));
887            assert!(code.is_known());
888        }
889
890        let last = ORDERED_ERROR_CODES
891            .last()
892            .expect("public error-code registry is non-empty")
893            .raw();
894
895        assert_eq!(last, 185);
896    }
897
898    #[test]
899    fn all_public_error_codes_round_trip_through_diagnostic_parts() {
900        let first = ORDERED_ERROR_CODES
901            .first()
902            .expect("public error-code registry is non-empty")
903            .raw();
904        let last = ORDERED_ERROR_CODES
905            .last()
906            .expect("public error-code registry is non-empty")
907            .raw();
908
909        for raw in first..=last {
910            let code = ErrorCode::from_raw(raw);
911            let diagnostic_code = code.diagnostic_code();
912            let diagnostic_detail = code.diagnostic_detail();
913            let rebuilt = ErrorCode::from_parts(diagnostic_code, diagnostic_detail);
914
915            assert_eq!(rebuilt.raw(), raw);
916
917            let diagnostic = code.diagnostic(ErrorOrigin::Runtime);
918
919            assert_eq!(diagnostic.code(), diagnostic_code);
920            assert_eq!(diagnostic.detail(), diagnostic_detail.as_ref());
921            assert_eq!(diagnostic.error_code().raw(), raw);
922        }
923    }
924
925    #[test]
926    fn invalid_raw_error_codes_fail_closed_to_runtime_internal() {
927        for raw in [0, 186, u16::MAX] {
928            let code = ErrorCode::from_raw(raw);
929
930            assert_eq!(ErrorCode::known(raw), None);
931            assert!(!code.is_known());
932            assert_eq!(code.diagnostic_code(), DiagnosticCode::RuntimeInternal);
933            assert_eq!(code.diagnostic_detail(), None);
934            assert_eq!(code.class(), ErrorClass::Internal);
935
936            let diagnostic = code.diagnostic(ErrorOrigin::Query);
937
938            assert_eq!(diagnostic.code(), DiagnosticCode::RuntimeInternal);
939            assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
940            assert_eq!(diagnostic.detail(), None);
941            assert_eq!(diagnostic.error_code(), ErrorCode::RUNTIME_INTERNAL);
942        }
943    }
944
945    #[test]
946    fn from_parts_requires_detail_to_match_broad_code() {
947        let detail = Some(DiagnosticDetail::UnsupportedSqlFeature {
948            feature: SqlFeatureCode::Join,
949        });
950
951        assert_eq!(
952            ErrorCode::from_parts(DiagnosticCode::QueryUnsupportedSqlFeature, detail),
953            ErrorCode::SQL_FEATURE_JOIN
954        );
955        assert_eq!(
956            ErrorCode::from_parts(DiagnosticCode::QueryPlan, detail),
957            ErrorCode::QUERY_PLAN
958        );
959    }
960
961    #[test]
962    fn detail_bearing_registry_entries_round_trip_directly() {
963        assert!(!DETAIL_ERROR_CODES.is_empty());
964
965        for &(code, diagnostic_code, detail) in DETAIL_ERROR_CODES {
966            assert_eq!(ErrorCode::from_parts(diagnostic_code, Some(detail)), code);
967            assert_eq!(code.diagnostic_code(), diagnostic_code);
968            assert_eq!(code.diagnostic_detail(), Some(detail));
969            assert_eq!(detail.diagnostic_code(), diagnostic_code);
970        }
971    }
972
973    #[test]
974    fn diagnostic_detail_reports_generated_broad_code() {
975        let detail = DiagnosticDetail::UnsupportedSqlFeature {
976            feature: SqlFeatureCode::Join,
977        };
978
979        assert_eq!(
980            detail.diagnostic_code(),
981            DiagnosticCode::QueryUnsupportedSqlFeature
982        );
983        assert_eq!(format!("{detail:?}"), "69");
984    }
985
986    #[test]
987    fn public_error_codes_reconstruct_shifted_details() {
988        assert_eq!(
989            ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD.diagnostic_code(),
990            DiagnosticCode::QueryUnknownAggregateTargetField
991        );
992        assert_eq!(
993            ErrorCode::SQL_FEATURE_JOIN.diagnostic_detail(),
994            Some(DiagnosticDetail::UnsupportedSqlFeature {
995                feature: SqlFeatureCode::Join,
996            })
997        );
998        assert_eq!(
999            ErrorCode::QUERY_PROJECTION_NUMERIC_LITERAL_REQUIRED.diagnostic_detail(),
1000            Some(DiagnosticDetail::QueryProjection {
1001                reason: QueryProjectionCode::NumericLiteralRequired,
1002            })
1003        );
1004        assert_eq!(
1005            ErrorCode::SQL_LOWERING_DISTINCT_ORDER_BY_PROJECTION.diagnostic_detail(),
1006            Some(DiagnosticDetail::SqlLowering {
1007                reason: SqlLoweringCode::DistinctOrderByProjection,
1008            })
1009        );
1010        assert_eq!(
1011            ErrorCode::SQL_WRITE_RETURNING_RESPONSE_TOO_LARGE.diagnostic_detail(),
1012            Some(DiagnosticDetail::SqlWriteBoundary {
1013                boundary: SqlWriteBoundaryCode::ReturningResponseTooLarge,
1014            })
1015        );
1016        assert_eq!(
1017            ErrorCode::SQL_WRITE_RETURNING_ROWS_TOO_MANY.diagnostic_detail(),
1018            Some(DiagnosticDetail::SqlWriteBoundary {
1019                boundary: SqlWriteBoundaryCode::ReturningRowsTooMany,
1020            })
1021        );
1022    }
1023}