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