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    ParameterBinding,
561    ParameterizedSchemaVersion,
562    PredicateStartsWithFirstArgument,
563    QuotedIdentifiers,
564    ReturningUnsupportedShape,
565    ScalarFunctionExpressionPosition,
566    ScaleTakingNumericFunctionExpressionPosition,
567    SearchedCaseGroupedOrderBy,
568    ShowColumnsModifiers,
569    ShowEntitiesModifiers,
570    ShowIndexesModifiers,
571    ShowMemoryModifiers,
572    ShowStoresModifiers,
573    ShowUnsupportedCommand,
574    SimpleCaseExpression,
575    StandaloneLiteralProjectionItem,
576    SupportedGroupedOrderByExpressionFamily,
577    SupportedOrderByExpressionFamily,
578    UnionIntersectExcept,
579    UnsupportedFunctionNamespace,
580    Update,
581    UpperFieldPredicateUnsupported,
582    WindowFunction,
583    With,
584    NumericScaleFunctionArguments,
585    OrderByFieldNotOrderable,
586}
587
588impl fmt::Debug for SqlFeatureCode {
589    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590        fmt_compact_code(f, *self as u16)
591    }
592}
593
594///
595/// SqlLoweringCode
596///
597/// Compact SQL lowering rejection identifier used after parsing succeeds but
598/// before a statement becomes canonical query intent.
599/// Variant order is wire-order significant for public error-code offsets.
600///
601
602#[repr(u16)]
603#[derive(Clone, Copy, Eq, Hash, PartialEq)]
604pub enum SqlLoweringCode {
605    EntityMismatch,
606    SelectProjectionShape,
607    SelectDistinct,
608    DistinctOrderByProjection,
609    GlobalAggregateProjection,
610    GlobalAggregateGroupBy,
611    SelectGroupByShape,
612    GroupedProjectionExplicitListRequired,
613    GroupedProjectionAggregateRequired,
614    GroupedProjectionNonGroupField,
615    GroupedProjectionScalarAfterAggregate,
616    HavingRequiresGroupBy,
617    SelectHavingShape,
618    AggregateInputExpressions,
619    WhereExpressionShape,
620    ParameterPlacement,
621    SqlDdlExecutionUnsupported,
622}
623
624impl fmt::Debug for SqlLoweringCode {
625    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626        fmt_compact_code(f, *self as u16)
627    }
628}
629
630///
631/// SqlSurfaceMismatchCode
632///
633/// Compact SQL endpoint surface mismatch identifier.
634/// Variant order is wire-order significant for public error-code offsets.
635///
636
637#[repr(u16)]
638#[derive(Clone, Copy, Eq, Hash, PartialEq)]
639pub enum SqlSurfaceMismatchCode {
640    QueryRejectsInsert,
641    QueryRejectsUpdate,
642    QueryRejectsDelete,
643    UpdateRejectsSelect,
644    UpdateRejectsExplain,
645    UpdateRejectsDescribe,
646    UpdateRejectsShowIndexes,
647    UpdateRejectsShowColumns,
648    UpdateRejectsShowEntities,
649    UpdateRejectsShowStores,
650    UpdateRejectsShowMemory,
651}
652
653impl fmt::Debug for SqlSurfaceMismatchCode {
654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655        fmt_compact_code(f, *self as u16)
656    }
657}
658
659///
660/// SqlWriteBoundaryCode
661///
662/// Compact SQL write fail-closed boundary identifier.
663/// Variant order is wire-order significant for public error-code offsets.
664///
665
666#[repr(u16)]
667#[derive(Clone, Copy, Eq, Hash, PartialEq)]
668pub enum SqlWriteBoundaryCode {
669    PrimaryKeyLiteralShape,
670    PrimaryKeyLiteralIncompatible,
671    MissingPrimaryKey,
672    MissingRequiredFields,
673    ExplicitManagedField,
674    ExplicitGeneratedField,
675    InsertSelectRequiresScalar,
676    InsertSelectAggregateProjection,
677    InsertSelectWidthMismatch,
678    UpdatePrimaryKeyMutation,
679    InvalidFieldLiteral,
680    UnknownReturningField,
681    DuplicateReturningField,
682    UpdateMissingWherePredicate,
683    WriteOrderByUnsupportedShape,
684    ReturningResponseTooLarge,
685    ReturningRowsTooMany,
686    StagedRowsTooMany,
687}
688
689impl fmt::Debug for SqlWriteBoundaryCode {
690    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
691        fmt_compact_code(f, *self as u16)
692    }
693}
694
695///
696/// SchemaDdlAdmissionCode
697///
698/// Compact SQL DDL admission rejection reason.
699/// Variant order is wire-order significant for public error-code offsets.
700///
701
702#[repr(u16)]
703#[derive(Clone, Copy, Eq, Hash, PartialEq)]
704pub enum SchemaDdlAdmissionCode {
705    MissingExpectedSchemaVersion,
706    MissingNextSchemaVersion,
707    StaleExpectedSchemaVersion,
708    InvalidExpectedSchemaVersion,
709    InvalidNextSchemaVersion,
710    AcceptedSchemaChangeWithoutVersionBump,
711    EmptyVersionBump,
712    VersionGap,
713    VersionRollback,
714    FingerprintMethodMismatch,
715    UnsupportedTransitionClass,
716    PhysicalRunnerMissing,
717    ValidationFailed,
718    PublicationRaceLost,
719    InvalidAddColumnDefault,
720    InvalidAlterColumnDefault,
721    GeneratedIndexDropRejected,
722    RequiredDropDefaultUnsupported,
723    GeneratedFieldDefaultChangeRejected,
724    GeneratedFieldNullabilityChangeRejected,
725    SetNotNullValidationFailed,
726}
727
728impl fmt::Debug for SchemaDdlAdmissionCode {
729    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730        fmt_compact_code(f, *self as u16)
731    }
732}
733
734///
735/// DiagnosticDetail
736///
737/// Small structured diagnostic payload for callers and CLI rendering.
738///
739
740#[remain::sorted]
741#[derive(Clone, Copy, Eq, PartialEq)]
742pub enum DiagnosticDetail {
743    QueryKind { kind: QueryErrorKind },
744    QueryProjection { reason: QueryProjectionCode },
745    QueryReadAdmission { reason: QueryReadAdmissionCode },
746    QueryResultShape { reason: QueryResultShapeCode },
747    RuntimeBoundary { boundary: RuntimeBoundaryCode },
748    RuntimeKind { kind: RuntimeErrorKind },
749    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
750    SqlLowering { reason: SqlLoweringCode },
751    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
752    SqlWriteBoundary { boundary: SqlWriteBoundaryCode },
753    UnsupportedSqlFeature { feature: SqlFeatureCode },
754}
755
756impl fmt::Debug for DiagnosticDetail {
757    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
758        fmt_compact_code(
759            f,
760            ErrorCode::from_parts(self.diagnostic_code(), Some(*self)).raw(),
761        )
762    }
763}
764
765///
766/// Diagnostic
767///
768/// Compact public diagnostic payload.
769///
770
771#[derive(Clone, Eq, PartialEq)]
772pub struct Diagnostic {
773    code: DiagnosticCode,
774    origin: ErrorOrigin,
775    detail: Option<DiagnosticDetail>,
776}
777
778impl Diagnostic {
779    /// Build a compact diagnostic from a code and optional structured detail.
780    #[must_use]
781    pub const fn new(
782        code: DiagnosticCode,
783        origin: ErrorOrigin,
784        detail: Option<DiagnosticDetail>,
785    ) -> Self {
786        Self {
787            code,
788            origin,
789            detail,
790        }
791    }
792
793    /// Build a compact diagnostic using the code's default origin.
794    #[must_use]
795    pub const fn from_code(code: DiagnosticCode) -> Self {
796        Self::new(code, code.origin(), None)
797    }
798
799    /// Return the stable diagnostic code.
800    #[must_use]
801    pub const fn code(&self) -> DiagnosticCode {
802        self.code
803    }
804
805    /// Return the diagnostic class.
806    #[must_use]
807    pub const fn class(&self) -> ErrorClass {
808        self.code.class()
809    }
810
811    /// Return the subsystem origin.
812    #[must_use]
813    pub const fn origin(&self) -> ErrorOrigin {
814        self.origin
815    }
816
817    /// Return structured diagnostic detail, when available.
818    #[must_use]
819    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
820        self.detail.as_ref()
821    }
822
823    /// Return the numeric public wire code for this diagnostic.
824    #[must_use]
825    pub const fn error_code(&self) -> ErrorCode {
826        ErrorCode::from_parts(self.code, self.detail)
827    }
828}
829
830impl fmt::Debug for Diagnostic {
831    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
832        write!(f, "{}@{}", self.error_code().raw(), self.origin.wire_code())
833    }
834}
835
836fn fmt_compact_code(f: &mut fmt::Formatter<'_>, raw: u16) -> fmt::Result {
837    write!(f, "{raw}")
838}
839
840#[cfg(test)]
841mod tests {
842    use super::{
843        Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorClass, ErrorCode, ErrorOrigin,
844        QueryProjectionCode, QueryReadAdmissionCode, SqlFeatureCode, SqlLoweringCode,
845        SqlWriteBoundaryCode,
846        registry::{DETAIL_ERROR_CODES, ORDERED_ERROR_CODES},
847    };
848
849    #[test]
850    fn diagnostic_from_code_uses_default_origin() {
851        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
852
853        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
854        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
855    }
856
857    #[test]
858    fn diagnostic_code_reports_broad_class() {
859        assert_eq!(
860            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
861            ErrorClass::Unsupported
862        );
863        assert_eq!(
864            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
865            ErrorClass::Unsupported
866        );
867        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
868        assert_eq!(
869            DiagnosticCode::StoreCorruption.class(),
870            ErrorClass::Corruption
871        );
872    }
873
874    #[test]
875    fn class_and_origin_wire_codes_round_trip() {
876        for (class, raw) in [
877            (ErrorClass::Query, 1),
878            (ErrorClass::Corruption, 2),
879            (ErrorClass::IncompatiblePersistedFormat, 3),
880            (ErrorClass::NotFound, 4),
881            (ErrorClass::Internal, 5),
882            (ErrorClass::Conflict, 6),
883            (ErrorClass::Unsupported, 7),
884            (ErrorClass::InvariantViolation, 8),
885        ] {
886            assert_eq!(class.wire_code(), raw);
887            assert_eq!(ErrorClass::from_wire_code(raw), Some(class));
888            assert_eq!(format!("{class:?}"), raw.to_string());
889        }
890
891        for (origin, raw) in [
892            (ErrorOrigin::Cursor, 1),
893            (ErrorOrigin::Executor, 2),
894            (ErrorOrigin::Identity, 3),
895            (ErrorOrigin::Index, 4),
896            (ErrorOrigin::Interface, 5),
897            (ErrorOrigin::Planner, 6),
898            (ErrorOrigin::Query, 7),
899            (ErrorOrigin::Recovery, 8),
900            (ErrorOrigin::Response, 9),
901            (ErrorOrigin::Runtime, 10),
902            (ErrorOrigin::Serialize, 11),
903            (ErrorOrigin::Store, 12),
904        ] {
905            assert_eq!(origin.wire_code(), raw);
906            assert_eq!(ErrorOrigin::from_known_wire_code(raw), Some(origin));
907            assert_eq!(ErrorOrigin::from_wire_code(raw), origin);
908            assert_eq!(format!("{origin:?}"), raw.to_string());
909        }
910
911        assert_eq!(ErrorClass::from_wire_code(0), None);
912        assert_eq!(ErrorOrigin::from_known_wire_code(0), None);
913        assert_eq!(ErrorOrigin::from_wire_code(0), ErrorOrigin::Runtime);
914    }
915
916    #[test]
917    fn public_error_codes_are_sequential() {
918        let first = ORDERED_ERROR_CODES
919            .first()
920            .expect("public error-code registry is non-empty")
921            .raw();
922
923        assert_eq!(first, 1);
924
925        for (index, code) in ORDERED_ERROR_CODES.iter().enumerate() {
926            let expected = first + u16::try_from(index).expect("test error-code index fits u16");
927            assert_eq!(code.raw(), expected);
928            assert_eq!(ErrorCode::known(code.raw()), Some(*code));
929            assert!(code.is_known());
930        }
931
932        let last = ORDERED_ERROR_CODES
933            .last()
934            .expect("public error-code registry is non-empty")
935            .raw();
936
937        assert_eq!(last, 204);
938    }
939
940    #[test]
941    fn all_public_error_codes_round_trip_through_diagnostic_parts() {
942        let first = ORDERED_ERROR_CODES
943            .first()
944            .expect("public error-code registry is non-empty")
945            .raw();
946        let last = ORDERED_ERROR_CODES
947            .last()
948            .expect("public error-code registry is non-empty")
949            .raw();
950
951        for raw in first..=last {
952            let code = ErrorCode::from_raw(raw);
953            let diagnostic_code = code.diagnostic_code();
954            let diagnostic_detail = code.diagnostic_detail();
955            let rebuilt = ErrorCode::from_parts(diagnostic_code, diagnostic_detail);
956
957            assert_eq!(rebuilt.raw(), raw);
958
959            let diagnostic = code.diagnostic(ErrorOrigin::Runtime);
960
961            assert_eq!(diagnostic.code(), diagnostic_code);
962            assert_eq!(diagnostic.detail(), diagnostic_detail.as_ref());
963            assert_eq!(diagnostic.error_code().raw(), raw);
964        }
965    }
966
967    #[test]
968    fn invalid_raw_error_codes_fail_closed_to_runtime_internal() {
969        for raw in [0, 205, u16::MAX] {
970            let code = ErrorCode::from_raw(raw);
971
972            assert_eq!(ErrorCode::known(raw), None);
973            assert!(!code.is_known());
974            assert_eq!(code.diagnostic_code(), DiagnosticCode::RuntimeInternal);
975            assert_eq!(code.diagnostic_detail(), None);
976            assert_eq!(code.class(), ErrorClass::Internal);
977
978            let diagnostic = code.diagnostic(ErrorOrigin::Query);
979
980            assert_eq!(diagnostic.code(), DiagnosticCode::RuntimeInternal);
981            assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
982            assert_eq!(diagnostic.detail(), None);
983            assert_eq!(diagnostic.error_code(), ErrorCode::RUNTIME_INTERNAL);
984        }
985    }
986
987    #[test]
988    fn from_parts_requires_detail_to_match_broad_code() {
989        let detail = Some(DiagnosticDetail::UnsupportedSqlFeature {
990            feature: SqlFeatureCode::Join,
991        });
992
993        assert_eq!(
994            ErrorCode::from_parts(DiagnosticCode::QueryUnsupportedSqlFeature, detail),
995            ErrorCode::SQL_FEATURE_JOIN
996        );
997        assert_eq!(
998            ErrorCode::from_parts(DiagnosticCode::QueryPlan, detail),
999            ErrorCode::QUERY_PLAN
1000        );
1001    }
1002
1003    #[test]
1004    fn detail_bearing_registry_entries_round_trip_directly() {
1005        assert!(!DETAIL_ERROR_CODES.is_empty());
1006
1007        for &(code, diagnostic_code, detail) in DETAIL_ERROR_CODES {
1008            assert_eq!(ErrorCode::from_parts(diagnostic_code, Some(detail)), code);
1009            assert_eq!(code.diagnostic_code(), diagnostic_code);
1010            assert_eq!(code.diagnostic_detail(), Some(detail));
1011            assert_eq!(detail.diagnostic_code(), diagnostic_code);
1012        }
1013    }
1014
1015    #[test]
1016    fn diagnostic_detail_reports_generated_broad_code() {
1017        let detail = DiagnosticDetail::UnsupportedSqlFeature {
1018            feature: SqlFeatureCode::Join,
1019        };
1020
1021        assert_eq!(
1022            detail.diagnostic_code(),
1023            DiagnosticCode::QueryUnsupportedSqlFeature
1024        );
1025        assert_eq!(format!("{detail:?}"), "69");
1026    }
1027
1028    #[test]
1029    fn public_error_codes_reconstruct_shifted_details() {
1030        assert_eq!(
1031            ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD.diagnostic_code(),
1032            DiagnosticCode::QueryUnknownAggregateTargetField
1033        );
1034        assert_eq!(
1035            ErrorCode::SQL_FEATURE_JOIN.diagnostic_detail(),
1036            Some(DiagnosticDetail::UnsupportedSqlFeature {
1037                feature: SqlFeatureCode::Join,
1038            })
1039        );
1040        assert_eq!(
1041            ErrorCode::QUERY_PROJECTION_NUMERIC_LITERAL_REQUIRED.diagnostic_detail(),
1042            Some(DiagnosticDetail::QueryProjection {
1043                reason: QueryProjectionCode::NumericLiteralRequired,
1044            })
1045        );
1046        assert_eq!(
1047            ErrorCode::QUERY_READ_PUBLIC_REQUIRES_LIMIT.diagnostic_detail(),
1048            Some(DiagnosticDetail::QueryReadAdmission {
1049                reason: QueryReadAdmissionCode::PublicQueryRequiresLimit,
1050            })
1051        );
1052        assert_eq!(
1053            ErrorCode::SQL_LOWERING_DISTINCT_ORDER_BY_PROJECTION.diagnostic_detail(),
1054            Some(DiagnosticDetail::SqlLowering {
1055                reason: SqlLoweringCode::DistinctOrderByProjection,
1056            })
1057        );
1058        assert_eq!(
1059            ErrorCode::SQL_WRITE_RETURNING_RESPONSE_TOO_LARGE.diagnostic_detail(),
1060            Some(DiagnosticDetail::SqlWriteBoundary {
1061                boundary: SqlWriteBoundaryCode::ReturningResponseTooLarge,
1062            })
1063        );
1064        assert_eq!(
1065            ErrorCode::SQL_WRITE_RETURNING_ROWS_TOO_MANY.diagnostic_detail(),
1066            Some(DiagnosticDetail::SqlWriteBoundary {
1067                boundary: SqlWriteBoundaryCode::ReturningRowsTooMany,
1068            })
1069        );
1070    }
1071}