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