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