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