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