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