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