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    SortRequiresMaterialization,
414    GroupedQueryRequiresLimits,
415    GroupedQueryExceedsBudget,
416    DiagnosticLaneDoesNotExecute,
417    ReturnedRowBoundExceedsPolicy,
418    PrimaryKeyInputExceedsPolicy,
419}
420
421impl fmt::Debug for QueryReadAdmissionCode {
422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423        fmt_compact_code(f, *self as u16)
424    }
425}
426
427///
428/// QueryResultShapeCode
429///
430/// Compact query-result shape mismatch identifier.
431/// Variant order is wire-order significant for public error-code offsets.
432///
433
434#[repr(u16)]
435#[derive(Clone, Copy, Eq, Hash, PartialEq)]
436pub enum QueryResultShapeCode {
437    ExpectedRows,
438    ExpectedGroupedRows,
439}
440
441impl fmt::Debug for QueryResultShapeCode {
442    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443        fmt_compact_code(f, *self as u16)
444    }
445}
446
447///
448/// RuntimeErrorKind
449///
450/// Public runtime error category.
451///
452
453#[repr(u16)]
454#[derive(Clone, Copy, Eq, Hash, PartialEq)]
455pub enum RuntimeErrorKind {
456    Corruption,
457    IncompatiblePersistedFormat,
458    InvariantViolation,
459    Conflict,
460    NotFound,
461    Unsupported,
462    Internal,
463}
464
465impl fmt::Debug for RuntimeErrorKind {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467        fmt_compact_code(f, *self as u16)
468    }
469}
470
471///
472/// RuntimeBoundaryCode
473///
474/// Compact public-runtime boundary identifier.
475/// Variant order is wire-order significant for public error-code offsets.
476///
477
478#[repr(u16)]
479#[derive(Clone, Copy, Eq, Hash, PartialEq)]
480pub enum RuntimeBoundaryCode {
481    SqlSurfaceControllerRequired,
482    SchemaSurfaceControllerRequired,
483    SqlQueryNoConfiguredEntities,
484    SqlQueryEntityNotConfigured,
485    SqlDdlTargetRequired,
486    SqlDdlEntityNotConfigured,
487    QueryResponseRowsRequired,
488    QueryResponseGroupedRowsRequired,
489    MutationResultEntityRequired,
490    MutationResultEntitiesRequired,
491    MutationResultIdRequired,
492    MutationResultIdsRequired,
493    RowProjectionFieldNotConfigured,
494    SqlIntrospectionDisabled,
495}
496
497impl fmt::Debug for RuntimeBoundaryCode {
498    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499        fmt_compact_code(f, *self as u16)
500    }
501}
502
503///
504/// SqlFeatureCode
505///
506/// Compact SQL feature identifier used by unsupported-feature diagnostics.
507/// Variant order is wire-order significant for public error-code offsets.
508///
509
510#[repr(u16)]
511#[derive(Clone, Copy, Eq, Hash, PartialEq)]
512pub enum SqlFeatureCode {
513    AggregateFilterClause,
514    AlterStatementBeyondAlterTable,
515    AlterTableAddColumnDuplicateDefault,
516    AlterTableAddColumnModifiers,
517    AlterTableAddStatementBeyondAddColumn,
518    AlterTableAlterColumnDropUnsupportedAction,
519    AlterTableAlterColumnModifiers,
520    AlterTableAlterColumnSetUnsupportedAction,
521    AlterTableAlterColumnUnsupportedAction,
522    AlterTableAlterStatementBeyondAlterColumn,
523    AlterTableDropColumnIfExistsSyntax,
524    AlterTableDropColumnModifiers,
525    AlterTableDropStatementBeyondDropColumn,
526    AlterTableRenameColumnMissingTo,
527    AlterTableRenameColumnModifiers,
528    AlterTableRenameStatementBeyondRenameColumn,
529    AlterTableUnsupportedOperation,
530    ColumnAlias,
531    CreateIndexIfNotExistsSyntax,
532    CreateIndexKeyOrderingModifiers,
533    CreateIndexModifiers,
534    CreateStatementBeyondCreateIndex,
535    DescribeModifier,
536    DdlSchemaVersionDuplicateExpectedClause,
537    DdlSchemaVersionDuplicateSetClause,
538    DropIndexModifiers,
539    DropIndexIfExistsSyntax,
540    DropStatementBeyondDropIndex,
541    ExpressionIndexUnsupportedFunction,
542    Having,
543    Insert,
544    Join,
545    LikePatternBeyondTrailingPrefix,
546    LowerFieldPredicateUnsupported,
547    MultiStatementSql,
548    NestedAggregateInput,
549    NestedProjectionFunctionInArithmetic,
550    OrderByUnsupportedForm,
551    Other,
552    PredicateStartsWithFirstArgument,
553    QuotedIdentifiers,
554    ReturningUnsupportedShape,
555    ScalarFunctionExpressionPosition,
556    ScaleTakingNumericFunctionExpressionPosition,
557    SearchedCaseGroupedOrderBy,
558    ShowColumnsModifiers,
559    ShowEntitiesModifiers,
560    ShowIndexesModifiers,
561    ShowMemoryModifiers,
562    ShowStoresModifiers,
563    ShowUnsupportedCommand,
564    SimpleCaseExpression,
565    StandaloneLiteralProjectionItem,
566    SupportedGroupedOrderByExpressionFamily,
567    SupportedOrderByExpressionFamily,
568    UnionIntersectExcept,
569    UnsupportedFunctionNamespace,
570    Update,
571    UpperFieldPredicateUnsupported,
572    WindowFunction,
573    With,
574    NumericScaleFunctionArguments,
575    OrderByFieldNotOrderable,
576}
577
578impl fmt::Debug for SqlFeatureCode {
579    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580        fmt_compact_code(f, *self as u16)
581    }
582}
583
584///
585/// SqlLoweringCode
586///
587/// Compact SQL lowering rejection identifier used after parsing succeeds but
588/// before a statement becomes canonical query intent.
589/// Variant order is wire-order significant for public error-code offsets.
590///
591
592#[repr(u16)]
593#[derive(Clone, Copy, Eq, Hash, PartialEq)]
594pub enum SqlLoweringCode {
595    EntityMismatch,
596    SelectProjectionShape,
597    SelectDistinct,
598    DistinctOrderByProjection,
599    GlobalAggregateProjection,
600    GlobalAggregateGroupBy,
601    SelectGroupByShape,
602    GroupedProjectionExplicitListRequired,
603    GroupedProjectionAggregateRequired,
604    GroupedProjectionNonGroupField,
605    GroupedProjectionScalarAfterAggregate,
606    HavingRequiresGroupBy,
607    SelectHavingShape,
608    AggregateInputExpressions,
609    WhereExpressionShape,
610    ParameterPlacement,
611    SqlDdlExecutionUnsupported,
612}
613
614impl fmt::Debug for SqlLoweringCode {
615    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616        fmt_compact_code(f, *self as u16)
617    }
618}
619
620///
621/// SqlSurfaceMismatchCode
622///
623/// Compact SQL endpoint surface mismatch 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 SqlSurfaceMismatchCode {
630    QueryRejectsInsert,
631    QueryRejectsUpdate,
632    QueryRejectsDelete,
633    UpdateRejectsSelect,
634    UpdateRejectsExplain,
635    UpdateRejectsDescribe,
636    UpdateRejectsShowIndexes,
637    UpdateRejectsShowColumns,
638    UpdateRejectsShowEntities,
639    UpdateRejectsShowStores,
640    UpdateRejectsShowMemory,
641}
642
643impl fmt::Debug for SqlSurfaceMismatchCode {
644    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
645        fmt_compact_code(f, *self as u16)
646    }
647}
648
649///
650/// SqlWriteBoundaryCode
651///
652/// Compact SQL write fail-closed boundary identifier.
653/// Variant order is wire-order significant for public error-code offsets.
654///
655
656#[repr(u16)]
657#[derive(Clone, Copy, Eq, Hash, PartialEq)]
658pub enum SqlWriteBoundaryCode {
659    PrimaryKeyLiteralShape,
660    PrimaryKeyLiteralIncompatible,
661    MissingPrimaryKey,
662    MissingRequiredFields,
663    ExplicitManagedField,
664    ExplicitGeneratedField,
665    InsertSelectRequiresScalar,
666    InsertSelectAggregateProjection,
667    InsertSelectWidthMismatch,
668    UpdatePrimaryKeyMutation,
669    InvalidFieldLiteral,
670    UnknownReturningField,
671    DuplicateReturningField,
672    UpdateMissingWherePredicate,
673    WriteOrderByUnsupportedShape,
674    ReturningResponseTooLarge,
675    ReturningRowsTooMany,
676    StagedRowsTooMany,
677}
678
679impl fmt::Debug for SqlWriteBoundaryCode {
680    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
681        fmt_compact_code(f, *self as u16)
682    }
683}
684
685///
686/// SchemaDdlAdmissionCode
687///
688/// Compact SQL DDL admission rejection reason.
689/// Variant order is wire-order significant for public error-code offsets.
690///
691
692#[repr(u16)]
693#[derive(Clone, Copy, Eq, Hash, PartialEq)]
694pub enum SchemaDdlAdmissionCode {
695    MissingExpectedSchemaVersion,
696    MissingNextSchemaVersion,
697    StaleExpectedSchemaVersion,
698    InvalidExpectedSchemaVersion,
699    InvalidNextSchemaVersion,
700    AcceptedSchemaChangeWithoutVersionBump,
701    EmptyVersionBump,
702    VersionGap,
703    VersionRollback,
704    FingerprintMethodMismatch,
705    UnsupportedTransitionClass,
706    PhysicalRunnerMissing,
707    ValidationFailed,
708    PublicationRaceLost,
709    InvalidAddColumnDefault,
710    InvalidAlterColumnDefault,
711    GeneratedIndexDropRejected,
712    RequiredDropDefaultUnsupported,
713    GeneratedFieldDefaultChangeRejected,
714    GeneratedFieldNullabilityChangeRejected,
715    SetNotNullValidationFailed,
716}
717
718impl fmt::Debug for SchemaDdlAdmissionCode {
719    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
720        fmt_compact_code(f, *self as u16)
721    }
722}
723
724///
725/// DiagnosticDetail
726///
727/// Small structured diagnostic payload for callers and CLI rendering.
728///
729
730#[remain::sorted]
731#[derive(Clone, Copy, Eq, PartialEq)]
732pub enum DiagnosticDetail {
733    QueryKind { kind: QueryErrorKind },
734    QueryProjection { reason: QueryProjectionCode },
735    QueryReadAdmission { reason: QueryReadAdmissionCode },
736    QueryResultShape { reason: QueryResultShapeCode },
737    RuntimeBoundary { boundary: RuntimeBoundaryCode },
738    RuntimeKind { kind: RuntimeErrorKind },
739    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
740    SqlLowering { reason: SqlLoweringCode },
741    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
742    SqlWriteBoundary { boundary: SqlWriteBoundaryCode },
743    UnsupportedSqlFeature { feature: SqlFeatureCode },
744}
745
746impl fmt::Debug for DiagnosticDetail {
747    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
748        fmt_compact_code(
749            f,
750            ErrorCode::from_parts(self.diagnostic_code(), Some(*self)).raw(),
751        )
752    }
753}
754
755///
756/// Diagnostic
757///
758/// Compact public diagnostic payload.
759///
760
761#[derive(Clone, Eq, PartialEq)]
762pub struct Diagnostic {
763    code: DiagnosticCode,
764    origin: ErrorOrigin,
765    detail: Option<DiagnosticDetail>,
766}
767
768impl Diagnostic {
769    /// Build a compact diagnostic from a code and optional structured detail.
770    #[must_use]
771    pub const fn new(
772        code: DiagnosticCode,
773        origin: ErrorOrigin,
774        detail: Option<DiagnosticDetail>,
775    ) -> Self {
776        Self {
777            code,
778            origin,
779            detail,
780        }
781    }
782
783    /// Build a compact diagnostic using the code's default origin.
784    #[must_use]
785    pub const fn from_code(code: DiagnosticCode) -> Self {
786        Self::new(code, code.origin(), None)
787    }
788
789    /// Return the stable diagnostic code.
790    #[must_use]
791    pub const fn code(&self) -> DiagnosticCode {
792        self.code
793    }
794
795    /// Return the diagnostic class.
796    #[must_use]
797    pub const fn class(&self) -> ErrorClass {
798        self.code.class()
799    }
800
801    /// Return the subsystem origin.
802    #[must_use]
803    pub const fn origin(&self) -> ErrorOrigin {
804        self.origin
805    }
806
807    /// Return structured diagnostic detail, when available.
808    #[must_use]
809    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
810        self.detail.as_ref()
811    }
812
813    /// Return the numeric public wire code for this diagnostic.
814    #[must_use]
815    pub const fn error_code(&self) -> ErrorCode {
816        ErrorCode::from_parts(self.code, self.detail)
817    }
818}
819
820impl fmt::Debug for Diagnostic {
821    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
822        write!(f, "{}@{}", self.error_code().raw(), self.origin.wire_code())
823    }
824}
825
826fn fmt_compact_code(f: &mut fmt::Formatter<'_>, raw: u16) -> fmt::Result {
827    write!(f, "{raw}")
828}
829
830#[cfg(test)]
831mod tests {
832    use super::{
833        Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorClass, ErrorCode, ErrorOrigin,
834        QueryProjectionCode, QueryReadAdmissionCode, SqlFeatureCode, SqlLoweringCode,
835        SqlWriteBoundaryCode,
836        registry::{DETAIL_ERROR_CODES, ORDERED_ERROR_CODES},
837    };
838
839    #[test]
840    fn diagnostic_from_code_uses_default_origin() {
841        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
842
843        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
844        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
845    }
846
847    #[test]
848    fn diagnostic_code_reports_broad_class() {
849        assert_eq!(
850            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
851            ErrorClass::Unsupported
852        );
853        assert_eq!(
854            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
855            ErrorClass::Unsupported
856        );
857        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
858        assert_eq!(
859            DiagnosticCode::StoreCorruption.class(),
860            ErrorClass::Corruption
861        );
862    }
863
864    #[test]
865    fn class_and_origin_wire_codes_round_trip() {
866        for (class, raw) in [
867            (ErrorClass::Query, 1),
868            (ErrorClass::Corruption, 2),
869            (ErrorClass::IncompatiblePersistedFormat, 3),
870            (ErrorClass::NotFound, 4),
871            (ErrorClass::Internal, 5),
872            (ErrorClass::Conflict, 6),
873            (ErrorClass::Unsupported, 7),
874            (ErrorClass::InvariantViolation, 8),
875        ] {
876            assert_eq!(class.wire_code(), raw);
877            assert_eq!(ErrorClass::from_wire_code(raw), Some(class));
878            assert_eq!(format!("{class:?}"), raw.to_string());
879        }
880
881        for (origin, raw) in [
882            (ErrorOrigin::Cursor, 1),
883            (ErrorOrigin::Executor, 2),
884            (ErrorOrigin::Identity, 3),
885            (ErrorOrigin::Index, 4),
886            (ErrorOrigin::Interface, 5),
887            (ErrorOrigin::Planner, 6),
888            (ErrorOrigin::Query, 7),
889            (ErrorOrigin::Recovery, 8),
890            (ErrorOrigin::Response, 9),
891            (ErrorOrigin::Runtime, 10),
892            (ErrorOrigin::Serialize, 11),
893            (ErrorOrigin::Store, 12),
894        ] {
895            assert_eq!(origin.wire_code(), raw);
896            assert_eq!(ErrorOrigin::from_known_wire_code(raw), Some(origin));
897            assert_eq!(ErrorOrigin::from_wire_code(raw), origin);
898            assert_eq!(format!("{origin:?}"), raw.to_string());
899        }
900
901        assert_eq!(ErrorClass::from_wire_code(0), None);
902        assert_eq!(ErrorOrigin::from_known_wire_code(0), None);
903        assert_eq!(ErrorOrigin::from_wire_code(0), ErrorOrigin::Runtime);
904    }
905
906    #[test]
907    fn public_error_codes_are_sequential() {
908        let first = ORDERED_ERROR_CODES
909            .first()
910            .expect("public error-code registry is non-empty")
911            .raw();
912
913        assert_eq!(first, 1);
914
915        for (index, code) in ORDERED_ERROR_CODES.iter().enumerate() {
916            let expected = first + u16::try_from(index).expect("test error-code index fits u16");
917            assert_eq!(code.raw(), expected);
918            assert_eq!(ErrorCode::known(code.raw()), Some(*code));
919            assert!(code.is_known());
920        }
921
922        let last = ORDERED_ERROR_CODES
923            .last()
924            .expect("public error-code registry is non-empty")
925            .raw();
926
927        assert_eq!(last, 194);
928    }
929
930    #[test]
931    fn all_public_error_codes_round_trip_through_diagnostic_parts() {
932        let first = ORDERED_ERROR_CODES
933            .first()
934            .expect("public error-code registry is non-empty")
935            .raw();
936        let last = ORDERED_ERROR_CODES
937            .last()
938            .expect("public error-code registry is non-empty")
939            .raw();
940
941        for raw in first..=last {
942            let code = ErrorCode::from_raw(raw);
943            let diagnostic_code = code.diagnostic_code();
944            let diagnostic_detail = code.diagnostic_detail();
945            let rebuilt = ErrorCode::from_parts(diagnostic_code, diagnostic_detail);
946
947            assert_eq!(rebuilt.raw(), raw);
948
949            let diagnostic = code.diagnostic(ErrorOrigin::Runtime);
950
951            assert_eq!(diagnostic.code(), diagnostic_code);
952            assert_eq!(diagnostic.detail(), diagnostic_detail.as_ref());
953            assert_eq!(diagnostic.error_code().raw(), raw);
954        }
955    }
956
957    #[test]
958    fn invalid_raw_error_codes_fail_closed_to_runtime_internal() {
959        for raw in [0, 203, u16::MAX] {
960            let code = ErrorCode::from_raw(raw);
961
962            assert_eq!(ErrorCode::known(raw), None);
963            assert!(!code.is_known());
964            assert_eq!(code.diagnostic_code(), DiagnosticCode::RuntimeInternal);
965            assert_eq!(code.diagnostic_detail(), None);
966            assert_eq!(code.class(), ErrorClass::Internal);
967
968            let diagnostic = code.diagnostic(ErrorOrigin::Query);
969
970            assert_eq!(diagnostic.code(), DiagnosticCode::RuntimeInternal);
971            assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
972            assert_eq!(diagnostic.detail(), None);
973            assert_eq!(diagnostic.error_code(), ErrorCode::RUNTIME_INTERNAL);
974        }
975    }
976
977    #[test]
978    fn from_parts_requires_detail_to_match_broad_code() {
979        let detail = Some(DiagnosticDetail::UnsupportedSqlFeature {
980            feature: SqlFeatureCode::Join,
981        });
982
983        assert_eq!(
984            ErrorCode::from_parts(DiagnosticCode::QueryUnsupportedSqlFeature, detail),
985            ErrorCode::SQL_FEATURE_JOIN
986        );
987        assert_eq!(
988            ErrorCode::from_parts(DiagnosticCode::QueryPlan, detail),
989            ErrorCode::QUERY_PLAN
990        );
991    }
992
993    #[test]
994    fn detail_bearing_registry_entries_round_trip_directly() {
995        assert!(!DETAIL_ERROR_CODES.is_empty());
996
997        for &(code, diagnostic_code, detail) in DETAIL_ERROR_CODES {
998            assert_eq!(ErrorCode::from_parts(diagnostic_code, Some(detail)), code);
999            assert_eq!(code.diagnostic_code(), diagnostic_code);
1000            assert_eq!(code.diagnostic_detail(), Some(detail));
1001            assert_eq!(detail.diagnostic_code(), diagnostic_code);
1002        }
1003    }
1004
1005    #[test]
1006    fn diagnostic_detail_reports_generated_broad_code() {
1007        let detail = DiagnosticDetail::UnsupportedSqlFeature {
1008            feature: SqlFeatureCode::Join,
1009        };
1010
1011        assert_eq!(
1012            detail.diagnostic_code(),
1013            DiagnosticCode::QueryUnsupportedSqlFeature
1014        );
1015        assert_eq!(format!("{detail:?}"), "69");
1016    }
1017
1018    #[test]
1019    fn public_error_codes_reconstruct_shifted_details() {
1020        assert_eq!(
1021            ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD.diagnostic_code(),
1022            DiagnosticCode::QueryUnknownAggregateTargetField
1023        );
1024        assert_eq!(
1025            ErrorCode::SQL_FEATURE_JOIN.diagnostic_detail(),
1026            Some(DiagnosticDetail::UnsupportedSqlFeature {
1027                feature: SqlFeatureCode::Join,
1028            })
1029        );
1030        assert_eq!(
1031            ErrorCode::QUERY_PROJECTION_NUMERIC_LITERAL_REQUIRED.diagnostic_detail(),
1032            Some(DiagnosticDetail::QueryProjection {
1033                reason: QueryProjectionCode::NumericLiteralRequired,
1034            })
1035        );
1036        assert_eq!(
1037            ErrorCode::QUERY_READ_PUBLIC_REQUIRES_LIMIT.diagnostic_detail(),
1038            Some(DiagnosticDetail::QueryReadAdmission {
1039                reason: QueryReadAdmissionCode::PublicQueryRequiresLimit,
1040            })
1041        );
1042        assert_eq!(
1043            ErrorCode::SQL_LOWERING_DISTINCT_ORDER_BY_PROJECTION.diagnostic_detail(),
1044            Some(DiagnosticDetail::SqlLowering {
1045                reason: SqlLoweringCode::DistinctOrderByProjection,
1046            })
1047        );
1048        assert_eq!(
1049            ErrorCode::SQL_WRITE_RETURNING_RESPONSE_TOO_LARGE.diagnostic_detail(),
1050            Some(DiagnosticDetail::SqlWriteBoundary {
1051                boundary: SqlWriteBoundaryCode::ReturningResponseTooLarge,
1052            })
1053        );
1054        assert_eq!(
1055            ErrorCode::SQL_WRITE_RETURNING_ROWS_TOO_MANY.diagnostic_detail(),
1056            Some(DiagnosticDetail::SqlWriteBoundary {
1057                boundary: SqlWriteBoundaryCode::ReturningRowsTooMany,
1058            })
1059        );
1060    }
1061}