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    StagedRowsTooMany,
648}
649
650impl fmt::Debug for SqlWriteBoundaryCode {
651    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652        fmt_compact_code(f, *self as u16)
653    }
654}
655
656///
657/// SchemaDdlAdmissionCode
658///
659/// Compact SQL DDL admission rejection reason.
660/// Variant order is wire-order significant for public error-code offsets.
661///
662
663#[repr(u16)]
664#[derive(Clone, Copy, Eq, Hash, PartialEq)]
665pub enum SchemaDdlAdmissionCode {
666    MissingExpectedSchemaVersion,
667    MissingNextSchemaVersion,
668    StaleExpectedSchemaVersion,
669    InvalidExpectedSchemaVersion,
670    InvalidNextSchemaVersion,
671    AcceptedSchemaChangeWithoutVersionBump,
672    EmptyVersionBump,
673    VersionGap,
674    VersionRollback,
675    FingerprintMethodMismatch,
676    UnsupportedTransitionClass,
677    PhysicalRunnerMissing,
678    ValidationFailed,
679    PublicationRaceLost,
680    InvalidAddColumnDefault,
681    InvalidAlterColumnDefault,
682    GeneratedIndexDropRejected,
683    RequiredDropDefaultUnsupported,
684    GeneratedFieldDefaultChangeRejected,
685    GeneratedFieldNullabilityChangeRejected,
686    SetNotNullValidationFailed,
687}
688
689impl fmt::Debug for SchemaDdlAdmissionCode {
690    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
691        fmt_compact_code(f, *self as u16)
692    }
693}
694
695///
696/// DiagnosticDetail
697///
698/// Small structured diagnostic payload for callers and CLI rendering.
699///
700
701#[remain::sorted]
702#[derive(Clone, Copy, Eq, PartialEq)]
703pub enum DiagnosticDetail {
704    QueryKind { kind: QueryErrorKind },
705    QueryProjection { reason: QueryProjectionCode },
706    QueryResultShape { reason: QueryResultShapeCode },
707    RuntimeBoundary { boundary: RuntimeBoundaryCode },
708    RuntimeKind { kind: RuntimeErrorKind },
709    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
710    SqlLowering { reason: SqlLoweringCode },
711    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
712    SqlWriteBoundary { boundary: SqlWriteBoundaryCode },
713    UnsupportedSqlFeature { feature: SqlFeatureCode },
714}
715
716impl fmt::Debug for DiagnosticDetail {
717    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
718        fmt_compact_code(
719            f,
720            ErrorCode::from_parts(self.diagnostic_code(), Some(*self)).raw(),
721        )
722    }
723}
724
725///
726/// Diagnostic
727///
728/// Compact public diagnostic payload.
729///
730
731#[derive(Clone, Eq, PartialEq)]
732pub struct Diagnostic {
733    code: DiagnosticCode,
734    origin: ErrorOrigin,
735    detail: Option<DiagnosticDetail>,
736}
737
738impl Diagnostic {
739    /// Build a compact diagnostic from a code and optional structured detail.
740    #[must_use]
741    pub const fn new(
742        code: DiagnosticCode,
743        origin: ErrorOrigin,
744        detail: Option<DiagnosticDetail>,
745    ) -> Self {
746        Self {
747            code,
748            origin,
749            detail,
750        }
751    }
752
753    /// Build a compact diagnostic using the code's default origin.
754    #[must_use]
755    pub const fn from_code(code: DiagnosticCode) -> Self {
756        Self::new(code, code.origin(), None)
757    }
758
759    /// Return the stable diagnostic code.
760    #[must_use]
761    pub const fn code(&self) -> DiagnosticCode {
762        self.code
763    }
764
765    /// Return the diagnostic class.
766    #[must_use]
767    pub const fn class(&self) -> ErrorClass {
768        self.code.class()
769    }
770
771    /// Return the subsystem origin.
772    #[must_use]
773    pub const fn origin(&self) -> ErrorOrigin {
774        self.origin
775    }
776
777    /// Return structured diagnostic detail, when available.
778    #[must_use]
779    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
780        self.detail.as_ref()
781    }
782
783    /// Return the numeric public wire code for this diagnostic.
784    #[must_use]
785    pub const fn error_code(&self) -> ErrorCode {
786        ErrorCode::from_parts(self.code, self.detail)
787    }
788}
789
790impl fmt::Debug for Diagnostic {
791    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
792        write!(f, "{}@{}", self.error_code().raw(), self.origin.wire_code())
793    }
794}
795
796fn fmt_compact_code(f: &mut fmt::Formatter<'_>, raw: u16) -> fmt::Result {
797    write!(f, "{raw}")
798}
799
800#[cfg(test)]
801mod tests {
802    use super::{
803        Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorClass, ErrorCode, ErrorOrigin,
804        QueryProjectionCode, SqlFeatureCode, SqlLoweringCode, SqlWriteBoundaryCode,
805        registry::{DETAIL_ERROR_CODES, ORDERED_ERROR_CODES},
806    };
807
808    #[test]
809    fn diagnostic_from_code_uses_default_origin() {
810        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
811
812        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
813        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
814    }
815
816    #[test]
817    fn diagnostic_code_reports_broad_class() {
818        assert_eq!(
819            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
820            ErrorClass::Unsupported
821        );
822        assert_eq!(
823            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
824            ErrorClass::Unsupported
825        );
826        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
827        assert_eq!(
828            DiagnosticCode::StoreCorruption.class(),
829            ErrorClass::Corruption
830        );
831    }
832
833    #[test]
834    fn class_and_origin_wire_codes_round_trip() {
835        for (class, raw) in [
836            (ErrorClass::Query, 1),
837            (ErrorClass::Corruption, 2),
838            (ErrorClass::IncompatiblePersistedFormat, 3),
839            (ErrorClass::NotFound, 4),
840            (ErrorClass::Internal, 5),
841            (ErrorClass::Conflict, 6),
842            (ErrorClass::Unsupported, 7),
843            (ErrorClass::InvariantViolation, 8),
844        ] {
845            assert_eq!(class.wire_code(), raw);
846            assert_eq!(ErrorClass::from_wire_code(raw), Some(class));
847            assert_eq!(format!("{class:?}"), raw.to_string());
848        }
849
850        for (origin, raw) in [
851            (ErrorOrigin::Cursor, 1),
852            (ErrorOrigin::Executor, 2),
853            (ErrorOrigin::Identity, 3),
854            (ErrorOrigin::Index, 4),
855            (ErrorOrigin::Interface, 5),
856            (ErrorOrigin::Planner, 6),
857            (ErrorOrigin::Query, 7),
858            (ErrorOrigin::Recovery, 8),
859            (ErrorOrigin::Response, 9),
860            (ErrorOrigin::Runtime, 10),
861            (ErrorOrigin::Serialize, 11),
862            (ErrorOrigin::Store, 12),
863        ] {
864            assert_eq!(origin.wire_code(), raw);
865            assert_eq!(ErrorOrigin::from_known_wire_code(raw), Some(origin));
866            assert_eq!(ErrorOrigin::from_wire_code(raw), origin);
867            assert_eq!(format!("{origin:?}"), raw.to_string());
868        }
869
870        assert_eq!(ErrorClass::from_wire_code(0), None);
871        assert_eq!(ErrorOrigin::from_known_wire_code(0), None);
872        assert_eq!(ErrorOrigin::from_wire_code(0), ErrorOrigin::Runtime);
873    }
874
875    #[test]
876    fn public_error_codes_are_sequential() {
877        let first = ORDERED_ERROR_CODES
878            .first()
879            .expect("public error-code registry is non-empty")
880            .raw();
881
882        assert_eq!(first, 1);
883
884        for (index, code) in ORDERED_ERROR_CODES.iter().enumerate() {
885            let expected = first + u16::try_from(index).expect("test error-code index fits u16");
886            assert_eq!(code.raw(), expected);
887            assert_eq!(ErrorCode::known(code.raw()), Some(*code));
888            assert!(code.is_known());
889        }
890
891        let last = ORDERED_ERROR_CODES
892            .last()
893            .expect("public error-code registry is non-empty")
894            .raw();
895
896        assert_eq!(last, 186);
897    }
898
899    #[test]
900    fn all_public_error_codes_round_trip_through_diagnostic_parts() {
901        let first = ORDERED_ERROR_CODES
902            .first()
903            .expect("public error-code registry is non-empty")
904            .raw();
905        let last = ORDERED_ERROR_CODES
906            .last()
907            .expect("public error-code registry is non-empty")
908            .raw();
909
910        for raw in first..=last {
911            let code = ErrorCode::from_raw(raw);
912            let diagnostic_code = code.diagnostic_code();
913            let diagnostic_detail = code.diagnostic_detail();
914            let rebuilt = ErrorCode::from_parts(diagnostic_code, diagnostic_detail);
915
916            assert_eq!(rebuilt.raw(), raw);
917
918            let diagnostic = code.diagnostic(ErrorOrigin::Runtime);
919
920            assert_eq!(diagnostic.code(), diagnostic_code);
921            assert_eq!(diagnostic.detail(), diagnostic_detail.as_ref());
922            assert_eq!(diagnostic.error_code().raw(), raw);
923        }
924    }
925
926    #[test]
927    fn invalid_raw_error_codes_fail_closed_to_runtime_internal() {
928        for raw in [0, 187, u16::MAX] {
929            let code = ErrorCode::from_raw(raw);
930
931            assert_eq!(ErrorCode::known(raw), None);
932            assert!(!code.is_known());
933            assert_eq!(code.diagnostic_code(), DiagnosticCode::RuntimeInternal);
934            assert_eq!(code.diagnostic_detail(), None);
935            assert_eq!(code.class(), ErrorClass::Internal);
936
937            let diagnostic = code.diagnostic(ErrorOrigin::Query);
938
939            assert_eq!(diagnostic.code(), DiagnosticCode::RuntimeInternal);
940            assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
941            assert_eq!(diagnostic.detail(), None);
942            assert_eq!(diagnostic.error_code(), ErrorCode::RUNTIME_INTERNAL);
943        }
944    }
945
946    #[test]
947    fn from_parts_requires_detail_to_match_broad_code() {
948        let detail = Some(DiagnosticDetail::UnsupportedSqlFeature {
949            feature: SqlFeatureCode::Join,
950        });
951
952        assert_eq!(
953            ErrorCode::from_parts(DiagnosticCode::QueryUnsupportedSqlFeature, detail),
954            ErrorCode::SQL_FEATURE_JOIN
955        );
956        assert_eq!(
957            ErrorCode::from_parts(DiagnosticCode::QueryPlan, detail),
958            ErrorCode::QUERY_PLAN
959        );
960    }
961
962    #[test]
963    fn detail_bearing_registry_entries_round_trip_directly() {
964        assert!(!DETAIL_ERROR_CODES.is_empty());
965
966        for &(code, diagnostic_code, detail) in DETAIL_ERROR_CODES {
967            assert_eq!(ErrorCode::from_parts(diagnostic_code, Some(detail)), code);
968            assert_eq!(code.diagnostic_code(), diagnostic_code);
969            assert_eq!(code.diagnostic_detail(), Some(detail));
970            assert_eq!(detail.diagnostic_code(), diagnostic_code);
971        }
972    }
973
974    #[test]
975    fn diagnostic_detail_reports_generated_broad_code() {
976        let detail = DiagnosticDetail::UnsupportedSqlFeature {
977            feature: SqlFeatureCode::Join,
978        };
979
980        assert_eq!(
981            detail.diagnostic_code(),
982            DiagnosticCode::QueryUnsupportedSqlFeature
983        );
984        assert_eq!(format!("{detail:?}"), "69");
985    }
986
987    #[test]
988    fn public_error_codes_reconstruct_shifted_details() {
989        assert_eq!(
990            ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD.diagnostic_code(),
991            DiagnosticCode::QueryUnknownAggregateTargetField
992        );
993        assert_eq!(
994            ErrorCode::SQL_FEATURE_JOIN.diagnostic_detail(),
995            Some(DiagnosticDetail::UnsupportedSqlFeature {
996                feature: SqlFeatureCode::Join,
997            })
998        );
999        assert_eq!(
1000            ErrorCode::QUERY_PROJECTION_NUMERIC_LITERAL_REQUIRED.diagnostic_detail(),
1001            Some(DiagnosticDetail::QueryProjection {
1002                reason: QueryProjectionCode::NumericLiteralRequired,
1003            })
1004        );
1005        assert_eq!(
1006            ErrorCode::SQL_LOWERING_DISTINCT_ORDER_BY_PROJECTION.diagnostic_detail(),
1007            Some(DiagnosticDetail::SqlLowering {
1008                reason: SqlLoweringCode::DistinctOrderByProjection,
1009            })
1010        );
1011        assert_eq!(
1012            ErrorCode::SQL_WRITE_RETURNING_RESPONSE_TOO_LARGE.diagnostic_detail(),
1013            Some(DiagnosticDetail::SqlWriteBoundary {
1014                boundary: SqlWriteBoundaryCode::ReturningResponseTooLarge,
1015            })
1016        );
1017        assert_eq!(
1018            ErrorCode::SQL_WRITE_RETURNING_ROWS_TOO_MANY.diagnostic_detail(),
1019            Some(DiagnosticDetail::SqlWriteBoundary {
1020                boundary: SqlWriteBoundaryCode::ReturningRowsTooMany,
1021            })
1022        );
1023    }
1024}