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