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
14mod fact;
15
16pub use fact::{DiagnosticFactTag, MAX_PUBLIC_DIAGNOSTIC_FACTS, pack_u32_pair, unpack_u32_pair};
17
18///
19/// DiagnosticCode
20///
21/// Stable machine-readable diagnostic reason.
22///
23
24#[remain::sorted]
25#[derive(Clone, Copy, Eq, Hash, PartialEq)]
26pub enum DiagnosticCode {
27    QueryAccessRequirement,
28    QueryIntent,
29    QueryInvalidContinuationCursor,
30    QueryNotFound,
31    QueryNotUnique,
32    QueryNumericNotRepresentable,
33    QueryNumericOverflow,
34    QueryPlan,
35    QueryReadAdmission,
36    QueryResultShapeMismatch,
37    QuerySqlSurfaceMismatch,
38    QuerySqlWriteBoundary,
39    QueryUnknownAggregateTargetField,
40    QueryUnorderedPagination,
41    QueryUnsupportedProjection,
42    QueryUnsupportedSqlFeature,
43    QueryValidate,
44    RuntimeConflict,
45    RuntimeCorruption,
46    RuntimeIncompatiblePersistedFormat,
47    RuntimeInternal,
48    RuntimeInvariantViolation,
49    RuntimeNotFound,
50    RuntimeUnsupported,
51    SchemaDdlAdmission,
52    StoreCorruption,
53    StoreInvariantViolation,
54    StoreNotFound,
55}
56
57impl DiagnosticCode {
58    /// Return the broad diagnostic class for this code.
59    #[must_use]
60    pub const fn class(self) -> ErrorClass {
61        match self {
62            Self::StoreCorruption | Self::RuntimeCorruption => ErrorClass::Corruption,
63            Self::RuntimeIncompatiblePersistedFormat => ErrorClass::IncompatiblePersistedFormat,
64            Self::QueryNotFound | Self::StoreNotFound | Self::RuntimeNotFound => {
65                ErrorClass::NotFound
66            }
67            Self::RuntimeConflict => ErrorClass::Conflict,
68            Self::QueryUnsupportedSqlFeature
69            | Self::QueryUnknownAggregateTargetField
70            | Self::QueryUnsupportedProjection
71            | Self::QueryResultShapeMismatch
72            | Self::QuerySqlSurfaceMismatch
73            | Self::QuerySqlWriteBoundary
74            | Self::RuntimeUnsupported => ErrorClass::Unsupported,
75            Self::StoreInvariantViolation | Self::RuntimeInvariantViolation => {
76                ErrorClass::InvariantViolation
77            }
78            Self::RuntimeInternal => ErrorClass::Internal,
79            Self::QueryValidate
80            | Self::QueryIntent
81            | Self::QueryPlan
82            | Self::QueryReadAdmission
83            | Self::QueryAccessRequirement
84            | Self::QueryUnorderedPagination
85            | Self::QueryInvalidContinuationCursor
86            | Self::QueryNotUnique
87            | Self::QueryNumericOverflow
88            | Self::QueryNumericNotRepresentable
89            | Self::SchemaDdlAdmission => ErrorClass::Query,
90        }
91    }
92
93    /// Return the default diagnostic origin for this code.
94    #[must_use]
95    pub const fn origin(self) -> ErrorOrigin {
96        match self {
97            Self::StoreNotFound | Self::StoreCorruption | Self::StoreInvariantViolation => {
98                ErrorOrigin::Store
99            }
100            Self::RuntimeCorruption
101            | Self::RuntimeIncompatiblePersistedFormat
102            | Self::RuntimeInvariantViolation
103            | Self::RuntimeConflict
104            | Self::RuntimeNotFound
105            | Self::RuntimeUnsupported
106            | Self::RuntimeInternal => ErrorOrigin::Runtime,
107            Self::QueryValidate
108            | Self::QueryIntent
109            | Self::QueryPlan
110            | Self::QueryReadAdmission
111            | Self::QueryAccessRequirement
112            | Self::QueryUnorderedPagination
113            | Self::QueryInvalidContinuationCursor
114            | Self::QueryNotFound
115            | Self::QueryNotUnique
116            | Self::QueryNumericOverflow
117            | Self::QueryNumericNotRepresentable
118            | Self::QueryUnknownAggregateTargetField
119            | Self::QueryUnsupportedProjection
120            | Self::QueryResultShapeMismatch
121            | Self::QueryUnsupportedSqlFeature
122            | Self::QuerySqlSurfaceMismatch
123            | Self::QuerySqlWriteBoundary
124            | Self::SchemaDdlAdmission => ErrorOrigin::Query,
125        }
126    }
127
128    /// Return the compact public wire code for this broad diagnostic reason.
129    #[must_use]
130    pub const fn error_code(self) -> ErrorCode {
131        match self {
132            Self::QueryValidate => ErrorCode::QUERY_VALIDATE,
133            Self::QueryIntent => ErrorCode::QUERY_INTENT,
134            Self::QueryPlan => ErrorCode::QUERY_PLAN,
135            Self::QueryReadAdmission => ErrorCode::QUERY_READ_ADMISSION,
136            Self::QueryAccessRequirement => ErrorCode::QUERY_ACCESS_REQUIREMENT,
137            Self::QueryUnorderedPagination => ErrorCode::QUERY_UNORDERED_PAGINATION,
138            Self::QueryInvalidContinuationCursor => ErrorCode::QUERY_INVALID_CONTINUATION_CURSOR,
139            Self::QueryNotFound => ErrorCode::QUERY_NOT_FOUND,
140            Self::QueryNotUnique => ErrorCode::QUERY_NOT_UNIQUE,
141            Self::QueryNumericOverflow => ErrorCode::QUERY_NUMERIC_OVERFLOW,
142            Self::QueryNumericNotRepresentable => ErrorCode::QUERY_NUMERIC_NOT_REPRESENTABLE,
143            Self::QueryUnknownAggregateTargetField => {
144                ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD
145            }
146            Self::QueryUnsupportedProjection => ErrorCode::QUERY_UNSUPPORTED_PROJECTION,
147            Self::QueryResultShapeMismatch => ErrorCode::QUERY_RESULT_SHAPE_MISMATCH,
148            Self::QueryUnsupportedSqlFeature => ErrorCode::QUERY_UNSUPPORTED_SQL_FEATURE,
149            Self::QuerySqlSurfaceMismatch => ErrorCode::QUERY_SQL_SURFACE_MISMATCH,
150            Self::QuerySqlWriteBoundary => ErrorCode::QUERY_SQL_WRITE_BOUNDARY,
151            Self::SchemaDdlAdmission => ErrorCode::SCHEMA_DDL_ADMISSION,
152            Self::StoreNotFound => ErrorCode::STORE_NOT_FOUND,
153            Self::StoreCorruption => ErrorCode::STORE_CORRUPTION,
154            Self::StoreInvariantViolation => ErrorCode::STORE_INVARIANT_VIOLATION,
155            Self::RuntimeCorruption => ErrorCode::RUNTIME_CORRUPTION,
156            Self::RuntimeIncompatiblePersistedFormat => {
157                ErrorCode::RUNTIME_INCOMPATIBLE_PERSISTED_FORMAT
158            }
159            Self::RuntimeInvariantViolation => ErrorCode::RUNTIME_INVARIANT_VIOLATION,
160            Self::RuntimeConflict => ErrorCode::RUNTIME_CONFLICT,
161            Self::RuntimeNotFound => ErrorCode::RUNTIME_NOT_FOUND,
162            Self::RuntimeUnsupported => ErrorCode::RUNTIME_UNSUPPORTED,
163            Self::RuntimeInternal => ErrorCode::RUNTIME_INTERNAL,
164        }
165    }
166}
167
168impl fmt::Debug for DiagnosticCode {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        fmt_compact_code(f, self.error_code().raw())
171    }
172}
173
174///
175/// ErrorCode
176///
177/// Stable numeric public error identity.
178///
179/// The public Candid `icydb::Error` stores this value as `nat16` so canister
180/// interfaces do not retain rich diagnostic enum labels. Rich diagnostics can
181/// still be reconstructed by host-side tooling from this leaf code. Before
182/// 1.0.0, the code space is hard-cut to a single compact sequential range.
183///
184
185#[derive(Clone, Copy, Eq, Hash, PartialEq)]
186pub struct ErrorCode(u16);
187
188mod registry;
189
190impl ErrorCode {
191    /// Build an error code from its raw public wire value.
192    #[must_use]
193    pub const fn from_raw(raw: u16) -> Self {
194        Self(raw)
195    }
196
197    /// Return the raw public wire value.
198    #[must_use]
199    pub const fn raw(self) -> u16 {
200        self.0
201    }
202}
203
204impl fmt::Debug for ErrorCode {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        fmt_compact_code(f, self.raw())
207    }
208}
209
210///
211/// ErrorClass
212///
213/// Broad diagnostic class used for recovery decisions.
214///
215
216#[remain::sorted]
217#[derive(Clone, Copy, Eq, Hash, PartialEq)]
218pub enum ErrorClass {
219    Conflict,
220    Corruption,
221    IncompatiblePersistedFormat,
222    Internal,
223    InvariantViolation,
224    NotFound,
225    Query,
226    Unsupported,
227}
228
229impl ErrorClass {
230    /// Return the compact public wire code for this diagnostic class.
231    #[must_use]
232    pub const fn wire_code(self) -> u8 {
233        match self {
234            Self::Query => 1,
235            Self::Corruption => 2,
236            Self::IncompatiblePersistedFormat => 3,
237            Self::NotFound => 4,
238            Self::Internal => 5,
239            Self::Conflict => 6,
240            Self::Unsupported => 7,
241            Self::InvariantViolation => 8,
242        }
243    }
244
245    /// Recover a diagnostic class from its compact public wire code.
246    #[must_use]
247    pub const fn from_wire_code(code: u8) -> Option<Self> {
248        match code {
249            1 => Some(Self::Query),
250            2 => Some(Self::Corruption),
251            3 => Some(Self::IncompatiblePersistedFormat),
252            4 => Some(Self::NotFound),
253            5 => Some(Self::Internal),
254            6 => Some(Self::Conflict),
255            7 => Some(Self::Unsupported),
256            8 => Some(Self::InvariantViolation),
257            _ => None,
258        }
259    }
260}
261
262impl fmt::Debug for ErrorClass {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        fmt_compact_code(f, u16::from(self.wire_code()))
265    }
266}
267
268///
269/// ErrorOrigin
270///
271/// Subsystem that owns the diagnostic.
272///
273
274#[remain::sorted]
275#[derive(Clone, Copy, Eq, Hash, PartialEq)]
276pub enum ErrorOrigin {
277    Cursor,
278    Executor,
279    Identity,
280    Index,
281    Interface,
282    Planner,
283    Query,
284    Recovery,
285    Response,
286    Runtime,
287    Serialize,
288    Store,
289}
290
291impl ErrorOrigin {
292    /// Return the compact public wire code for this diagnostic origin.
293    #[must_use]
294    pub const fn wire_code(self) -> u8 {
295        match self {
296            Self::Cursor => 1,
297            Self::Executor => 2,
298            Self::Identity => 3,
299            Self::Index => 4,
300            Self::Interface => 5,
301            Self::Planner => 6,
302            Self::Query => 7,
303            Self::Recovery => 8,
304            Self::Response => 9,
305            Self::Runtime => 10,
306            Self::Serialize => 11,
307            Self::Store => 12,
308        }
309    }
310
311    /// Recover a known diagnostic origin from its compact public wire code.
312    #[must_use]
313    pub const fn from_known_wire_code(code: u8) -> Option<Self> {
314        match code {
315            1 => Some(Self::Cursor),
316            2 => Some(Self::Executor),
317            3 => Some(Self::Identity),
318            4 => Some(Self::Index),
319            5 => Some(Self::Interface),
320            6 => Some(Self::Planner),
321            7 => Some(Self::Query),
322            8 => Some(Self::Recovery),
323            9 => Some(Self::Response),
324            10 => Some(Self::Runtime),
325            11 => Some(Self::Serialize),
326            12 => Some(Self::Store),
327            _ => None,
328        }
329    }
330
331    /// Recover a diagnostic origin from its compact public wire code.
332    ///
333    /// Unknown origin codes fail closed to `Runtime`, matching the public
334    /// boundary behavior used by the Candid facade.
335    #[must_use]
336    pub const fn from_wire_code(code: u8) -> Self {
337        match Self::from_known_wire_code(code) {
338            Some(origin) => origin,
339            None => Self::Runtime,
340        }
341    }
342}
343
344impl fmt::Debug for ErrorOrigin {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        fmt_compact_code(f, u16::from(self.wire_code()))
347    }
348}
349
350///
351/// QueryErrorKind
352///
353/// Public query error category.
354///
355
356#[repr(u16)]
357#[derive(Clone, Copy, Eq, Hash, PartialEq)]
358pub enum QueryErrorKind {
359    Validate,
360    Intent,
361    Plan,
362    AccessRequirement,
363    UnorderedPagination,
364    InvalidContinuationCursor,
365    NotFound,
366    NotUnique,
367}
368
369impl fmt::Debug for QueryErrorKind {
370    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371        fmt_compact_code(f, *self as u16)
372    }
373}
374
375///
376/// QueryProjectionCode
377///
378/// Compact query projection admission/runtime identifier.
379/// Variant order is wire-order significant for public error-code offsets.
380///
381
382#[repr(u16)]
383#[derive(Clone, Copy, Eq, Hash, PartialEq)]
384pub enum QueryProjectionCode {
385    NumericLiteralRequired,
386    NumericScaleArguments,
387    NestedFieldPathPreview,
388    CaseConditionBooleanRequired,
389    NumericInputRequired,
390    TextOrBlobInputRequired,
391    TextInputRequired,
392    TextOrNullArgumentRequired,
393    IntegerOrNullArgumentRequired,
394    UnaryOperandIncompatible,
395    BinaryOperandsIncompatible,
396}
397
398impl fmt::Debug for QueryProjectionCode {
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        fmt_compact_code(f, *self as u16)
401    }
402}
403
404///
405/// QueryReadAdmissionCode
406///
407/// Compact read-admission rejection identifier.
408/// Variant order is wire-order significant for public error-code offsets.
409///
410
411#[repr(u16)]
412#[derive(Clone, Copy, Eq, Hash, PartialEq)]
413pub enum QueryReadAdmissionCode {
414    PublicQueryRequiresLimit,
415    PublicQueryRequiresIndex,
416    UnboundedFullScanRejected,
417    SortRequiresMaterialization,
418    GroupedQueryRequiresLimits,
419    GroupedQueryExceedsBudget,
420    DiagnosticLaneDoesNotExecute,
421    ReturnedRowBoundExceedsPolicy,
422    PrimaryKeyInputExceedsPolicy,
423}
424
425impl fmt::Debug for QueryReadAdmissionCode {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        fmt_compact_code(f, *self as u16)
428    }
429}
430
431///
432/// QueryResultShapeCode
433///
434/// Compact query-result shape mismatch identifier.
435/// Variant order is wire-order significant for public error-code offsets.
436///
437
438#[repr(u16)]
439#[derive(Clone, Copy, Eq, Hash, PartialEq)]
440pub enum QueryResultShapeCode {
441    ExpectedRows,
442    ExpectedGroupedRows,
443}
444
445impl fmt::Debug for QueryResultShapeCode {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        fmt_compact_code(f, *self as u16)
448    }
449}
450
451///
452/// RuntimeErrorKind
453///
454/// Public runtime error category.
455///
456
457#[repr(u16)]
458#[derive(Clone, Copy, Eq, Hash, PartialEq)]
459pub enum RuntimeErrorKind {
460    Corruption,
461    IncompatiblePersistedFormat,
462    InvariantViolation,
463    Conflict,
464    NotFound,
465    Unsupported,
466    Internal,
467}
468
469impl fmt::Debug for RuntimeErrorKind {
470    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471        fmt_compact_code(f, *self as u16)
472    }
473}
474
475///
476/// RuntimeBoundaryCode
477///
478/// Compact public-runtime boundary identifier.
479/// Variant order is wire-order significant for public error-code offsets.
480///
481
482#[repr(u16)]
483#[derive(Clone, Copy, Eq, Hash, PartialEq)]
484pub enum RuntimeBoundaryCode {
485    SqlSurfaceControllerRequired,
486    SchemaSurfaceControllerRequired,
487    SqlQueryNoConfiguredEntities,
488    SqlQueryEntityNotConfigured,
489    SqlDdlTargetRequired,
490    SqlDdlEntityNotConfigured,
491    QueryResponseRowsRequired,
492    QueryResponseGroupedRowsRequired,
493    RowProjectionFieldNotConfigured,
494    SqlIntrospectionDisabled,
495    /// A complete accepted mutation omitted a required field.
496    MutationRequiredFieldMissing,
497    /// A logical write would move an accepted managed timestamp backward.
498    MutationManagedTimestampRegression,
499    /// A persisted row's stamp falls outside the accepted layout window.
500    PersistedRowLayoutOutsideAcceptedWindow,
501    /// A persisted row's physical slot count disagrees with its layout stamp.
502    PersistedRowSlotCountMismatch,
503    /// A generated field would collide with an accepted DDL-owned slot.
504    GeneratedFieldAfterDdlField,
505    /// A journaled mutation cannot reserve a representable post-commit revision.
506    JournalMutationRevisionExhausted,
507    /// A final canonical after-image violates one accepted row constraint or gate.
508    ConstraintViolation,
509    /// Accepted row-constraint metadata or its compiled program is inconsistent.
510    AcceptedRowConstraintProgramCorrupt,
511    /// A write conflicts with one incomplete accepted constraint activation.
512    ConstraintActivationWriteBlocked,
513    /// A live generated constraint activation no longer matches its proposal.
514    GeneratedConstraintActivationStale,
515    /// A caller explicitly authored a field owned by accepted database policy.
516    MutationDatabaseOwnedFieldExplicit,
517    /// A mixed structural mutation batch contained no operations.
518    MutationBatchEmpty,
519    /// A mixed structural mutation batch exceeded its operation-count bound.
520    MutationBatchTooManyItems,
521    /// A mixed structural mutation batch exceeded its staged-byte bound.
522    MutationBatchStagedBytesExceeded,
523    /// A mixed structural mutation result exceeded its encoded response bound.
524    MutationBatchResultBytesExceeded,
525    /// A mixed structural mutation batch resolved to more than one accepted entity.
526    MutationBatchEntityMismatch,
527    /// More than one mixed structural operation targeted the same accepted key.
528    MutationBatchDuplicateKey,
529    /// An operational report or reset endpoint requires a controller caller.
530    OperationalSurfaceControllerRequired,
531}
532
533impl fmt::Debug for RuntimeBoundaryCode {
534    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535        fmt_compact_code(f, *self as u16)
536    }
537}
538
539///
540/// SqlFeatureCode
541///
542/// Compact SQL feature identifier used by unsupported-feature diagnostics.
543/// Variant order is wire-order significant for public error-code offsets.
544///
545
546#[repr(u16)]
547#[derive(Clone, Copy, Eq, Hash, PartialEq)]
548pub enum SqlFeatureCode {
549    AggregateFilterClause,
550    AlterStatementBeyondAlterTable,
551    AlterTableAddColumnDuplicateDefault,
552    AlterTableAddColumnModifiers,
553    AlterTableAddStatementBeyondAddColumn,
554    AlterTableAlterColumnDropUnsupportedAction,
555    AlterTableAlterColumnModifiers,
556    AlterTableAlterColumnSetUnsupportedAction,
557    AlterTableAlterColumnUnsupportedAction,
558    AlterTableAlterStatementBeyondAlterColumn,
559    AlterTableDropColumnIfExistsSyntax,
560    AlterTableDropColumnModifiers,
561    AlterTableDropStatementBeyondDropColumn,
562    AlterTableRenameColumnMissingTo,
563    AlterTableRenameColumnModifiers,
564    AlterTableRenameStatementBeyondRenameColumn,
565    AlterTableUnsupportedOperation,
566    ColumnAlias,
567    CreateIndexIfNotExistsSyntax,
568    CreateIndexKeyOrderingModifiers,
569    CreateIndexModifiers,
570    CreateStatementBeyondCreateIndex,
571    DescribeModifier,
572    DdlSchemaVersionDuplicateExpectedClause,
573    DdlSchemaVersionDuplicateSetClause,
574    DropIndexModifiers,
575    DropIndexIfExistsSyntax,
576    DropStatementBeyondDropIndex,
577    ExpressionIndexUnsupportedFunction,
578    Having,
579    Insert,
580    Join,
581    LikePatternBeyondTrailingPrefix,
582    LowerFieldPredicateUnsupported,
583    MultiStatementSql,
584    NestedAggregateInput,
585    NestedProjectionFunctionInArithmetic,
586    OrderByUnsupportedForm,
587    Other,
588    PredicateStartsWithFirstArgument,
589    QuotedIdentifiers,
590    ReturningUnsupportedShape,
591    ScalarFunctionExpressionPosition,
592    ScaleTakingNumericFunctionExpressionPosition,
593    SearchedCaseGroupedOrderBy,
594    ShowColumnsModifiers,
595    ShowEntitiesModifiers,
596    ShowIndexesModifiers,
597    ShowMemoryModifiers,
598    ShowStoresModifiers,
599    ShowUnsupportedCommand,
600    SimpleCaseExpression,
601    StandaloneLiteralProjectionItem,
602    SupportedGroupedOrderByExpressionFamily,
603    SupportedOrderByExpressionFamily,
604    UnionIntersectExcept,
605    UnsupportedFunctionNamespace,
606    Update,
607    UpperFieldPredicateUnsupported,
608    WindowFunction,
609    With,
610    NumericScaleFunctionArguments,
611    OrderByFieldNotOrderable,
612    ShowConstraintsModifiers,
613    AlterTableAddConstraintBeyondCheck,
614    AlterTableAddConstraintModifiers,
615    AlterTableDropConstraintIfExistsSyntax,
616    AlterTableDropConstraintModifiers,
617    AlterTableValidateBeyondConstraint,
618    AlterTableValidateConstraintModifiers,
619}
620
621impl fmt::Debug for SqlFeatureCode {
622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623        fmt_compact_code(f, *self as u16)
624    }
625}
626
627///
628/// SqlLoweringCode
629///
630/// Compact SQL lowering rejection identifier used after parsing succeeds but
631/// before a statement becomes canonical query intent.
632/// Variant order is wire-order significant for public error-code offsets.
633///
634
635#[repr(u16)]
636#[derive(Clone, Copy, Eq, Hash, PartialEq)]
637pub enum SqlLoweringCode {
638    EntityMismatch,
639    SelectProjectionShape,
640    SelectDistinct,
641    DistinctOrderByProjection,
642    GlobalAggregateProjection,
643    GlobalAggregateGroupBy,
644    SelectGroupByShape,
645    GroupedProjectionExplicitListRequired,
646    GroupedProjectionAggregateRequired,
647    GroupedProjectionNonGroupField,
648    GroupedProjectionScalarAfterAggregate,
649    HavingRequiresGroupBy,
650    SelectHavingShape,
651    AggregateInputExpressions,
652    WhereExpressionShape,
653    ParameterPlacement,
654    SqlDdlExecutionUnsupported,
655}
656
657impl fmt::Debug for SqlLoweringCode {
658    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659        fmt_compact_code(f, *self as u16)
660    }
661}
662
663///
664/// SqlSurfaceMismatchCode
665///
666/// Compact SQL endpoint surface mismatch identifier.
667/// Variant order is wire-order significant for public error-code offsets.
668///
669
670#[repr(u16)]
671#[derive(Clone, Copy, Eq, Hash, PartialEq)]
672pub enum SqlSurfaceMismatchCode {
673    QueryRejectsInsert,
674    QueryRejectsUpdate,
675    QueryRejectsDelete,
676    MutationRejectsSelect,
677    MutationRejectsExplain,
678    MutationRejectsDescribe,
679    MutationRejectsShowIndexes,
680    MutationRejectsShowColumns,
681    MutationRejectsShowEntities,
682    MutationRejectsShowStores,
683    MutationRejectsShowMemory,
684    MutationRequiresExplicitUpdateIntent,
685    MutationRejectsShowConstraints,
686}
687
688impl fmt::Debug for SqlSurfaceMismatchCode {
689    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690        fmt_compact_code(f, *self as u16)
691    }
692}
693
694///
695/// SqlWriteBoundaryCode
696///
697/// Compact SQL write fail-closed boundary identifier.
698/// Variant order is wire-order significant for public error-code offsets.
699///
700
701#[repr(u16)]
702#[derive(Clone, Copy, Eq, Hash, PartialEq)]
703pub enum SqlWriteBoundaryCode {
704    PrimaryKeyLiteralShape,
705    PrimaryKeyLiteralIncompatible,
706    MissingPrimaryKey,
707    MissingRequiredFields,
708    ExplicitManagedField,
709    ExplicitGeneratedField,
710    InsertSelectRequiresScalar,
711    InsertSelectAggregateProjection,
712    InsertSelectWidthMismatch,
713    UpdatePrimaryKeyMutation,
714    InvalidFieldLiteral,
715    UnknownReturningField,
716    DuplicateReturningField,
717    UpdateMissingWherePredicate,
718    WriteOrderByUnsupportedShape,
719    ReturningResponseTooLarge,
720    ReturningRowsTooMany,
721    StagedRowsTooMany,
722    InsertDefaultRequiredField,
723    UpdateDefaultRequiredField,
724    UpdateDefaultDatabaseOwnedField,
725    ExactUpdateAssertionRequired,
726    ExactUpdateAssertionTooHigh,
727    ExactUpdateAffectedRowsExceeded,
728    ExactUpdateWindowUnsupported,
729    ExactUpdateScanBudgetExceeded,
730    ResumableUpdateWindowUnsupported,
731    ResumableUpdateReturningUnsupported,
732    ResumableUpdateRequiresJournaledStore,
733    ResumableUpdateAssignedFieldHasGlobalConstraint,
734    ResumableUpdateScopeDependsOnAssignedField,
735    ResumableUpdateScopeDependencyUnknown,
736    ResumableUpdateContinuationMalformed,
737    ResumableUpdateContinuationTargetMismatch,
738    ResumableUpdateContinuationSchemaMismatch,
739    ResumableUpdateContinuationScopeMismatch,
740    ResumableUpdateContinuationPatchMismatch,
741    ResumableUpdateContinuationBatchPolicyMismatch,
742    ResumableUpdateSingleRowResourceExceeded,
743    ResumableUpdateManagedFieldHasGlobalConstraint,
744    ResumableUpdateContinuationOperationMismatch,
745}
746
747impl fmt::Debug for SqlWriteBoundaryCode {
748    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749        fmt_compact_code(f, *self as u16)
750    }
751}
752
753///
754/// SchemaDdlAdmissionCode
755///
756/// Compact SQL DDL admission rejection reason.
757/// Variant order is wire-order significant for public error-code offsets.
758///
759
760#[repr(u16)]
761#[derive(Clone, Copy, Eq, Hash, PartialEq)]
762pub enum SchemaDdlAdmissionCode {
763    MissingExpectedSchemaVersion,
764    MissingNextSchemaVersion,
765    StaleExpectedSchemaVersion,
766    InvalidExpectedSchemaVersion,
767    InvalidNextSchemaVersion,
768    AcceptedSchemaChangeWithoutVersionBump,
769    EmptyVersionBump,
770    VersionGap,
771    VersionRollback,
772    FingerprintMethodMismatch,
773    UnsupportedTransitionClass,
774    PhysicalRunnerMissing,
775    ValidationFailed,
776    PublicationRaceLost,
777    InvalidAddColumnDefault,
778    InvalidAlterColumnDefault,
779    GeneratedIndexDropRejected,
780    SchemaRewriteRequiresMigration,
781    SchemaTransitionBudgetExceeded,
782    GeneratedFieldDefaultChangeRejected,
783    GeneratedFieldNullabilityChangeRejected,
784    RowLayoutVersionExhausted,
785}
786
787impl fmt::Debug for SchemaDdlAdmissionCode {
788    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
789        fmt_compact_code(f, *self as u16)
790    }
791}
792
793/// Machine-readable source-migration rejection or lifecycle finding.
794#[repr(u16)]
795#[derive(Clone, Copy, Eq, Hash, PartialEq)]
796pub enum SchemaMigrationCode {
797    Unadopted,
798    MissingMigration,
799    VersionGap,
800    Downgrade,
801    EmptyEntityVersionBump,
802    DuplicateEntityTransition,
803    StaleAcceptedHead,
804    PlanChanged,
805    DuplicateRenameSource,
806    DuplicateRenameTarget,
807    UnknownFromObject,
808    UnknownToObject,
809    KindMismatch,
810    IdentityConflict,
811    IncompleteRenameCoverage,
812    UnexplainedSchemaDifference,
813    UnsupportedTransform,
814    TransformFinding,
815    UniqueIndexFinding,
816    RelationFinding,
817    ConstraintFinding,
818    PhysicalRunnerMissing,
819    MigrationInProgress,
820    AbortTooLate,
821    ProgressCorrupt,
822    CandidateMismatch,
823    PublicationRaceLost,
824}
825
826impl SchemaMigrationCode {
827    /// Return the broad diagnostic category for this migration result.
828    #[must_use]
829    pub const fn diagnostic_code(self) -> DiagnosticCode {
830        match self {
831            Self::StaleAcceptedHead
832            | Self::PlanChanged
833            | Self::IdentityConflict
834            | Self::MigrationInProgress
835            | Self::AbortTooLate
836            | Self::PublicationRaceLost => DiagnosticCode::RuntimeConflict,
837            Self::ProgressCorrupt | Self::CandidateMismatch => DiagnosticCode::RuntimeCorruption,
838            Self::Unadopted
839            | Self::MissingMigration
840            | Self::VersionGap
841            | Self::Downgrade
842            | Self::EmptyEntityVersionBump
843            | Self::DuplicateEntityTransition
844            | Self::DuplicateRenameSource
845            | Self::DuplicateRenameTarget
846            | Self::UnknownFromObject
847            | Self::UnknownToObject
848            | Self::KindMismatch
849            | Self::IncompleteRenameCoverage
850            | Self::UnexplainedSchemaDifference
851            | Self::UnsupportedTransform
852            | Self::TransformFinding
853            | Self::UniqueIndexFinding
854            | Self::RelationFinding
855            | Self::ConstraintFinding
856            | Self::PhysicalRunnerMissing => DiagnosticCode::RuntimeUnsupported,
857        }
858    }
859}
860
861impl fmt::Debug for SchemaMigrationCode {
862    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
863        fmt_compact_code(f, *self as u16)
864    }
865}
866
867///
868/// DiagnosticDetail
869///
870/// Small structured diagnostic payload for callers and CLI rendering.
871///
872
873#[remain::sorted]
874#[derive(Clone, Copy, Eq, PartialEq)]
875pub enum DiagnosticDetail {
876    QueryKind { kind: QueryErrorKind },
877    QueryProjection { reason: QueryProjectionCode },
878    QueryReadAdmission { reason: QueryReadAdmissionCode },
879    QueryResultShape { reason: QueryResultShapeCode },
880    RuntimeBoundary { boundary: RuntimeBoundaryCode },
881    RuntimeKind { kind: RuntimeErrorKind },
882    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
883    SchemaMigration { reason: SchemaMigrationCode },
884    SqlLowering { reason: SqlLoweringCode },
885    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
886    SqlWriteBoundary { boundary: SqlWriteBoundaryCode },
887    UnsupportedSqlFeature { feature: SqlFeatureCode },
888}
889
890impl fmt::Debug for DiagnosticDetail {
891    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
892        fmt_compact_code(
893            f,
894            ErrorCode::from_parts(self.diagnostic_code(), Some(*self)).raw(),
895        )
896    }
897}
898
899///
900/// Diagnostic
901///
902/// Compact public diagnostic payload.
903///
904
905#[derive(Clone, Eq, PartialEq)]
906pub struct Diagnostic {
907    code: DiagnosticCode,
908    origin: ErrorOrigin,
909    detail: Option<DiagnosticDetail>,
910}
911
912impl Diagnostic {
913    /// Build a compact diagnostic from a code and optional structured detail.
914    #[must_use]
915    pub const fn new(
916        code: DiagnosticCode,
917        origin: ErrorOrigin,
918        detail: Option<DiagnosticDetail>,
919    ) -> Self {
920        Self {
921            code,
922            origin,
923            detail,
924        }
925    }
926
927    /// Build a compact diagnostic using the code's default origin.
928    #[must_use]
929    pub const fn from_code(code: DiagnosticCode) -> Self {
930        Self::new(code, code.origin(), None)
931    }
932
933    /// Return the stable diagnostic code.
934    #[must_use]
935    pub const fn code(&self) -> DiagnosticCode {
936        self.code
937    }
938
939    /// Return the diagnostic class.
940    #[must_use]
941    pub const fn class(&self) -> ErrorClass {
942        self.code.class()
943    }
944
945    /// Return the subsystem origin.
946    #[must_use]
947    pub const fn origin(&self) -> ErrorOrigin {
948        self.origin
949    }
950
951    /// Return structured diagnostic detail, when available.
952    #[must_use]
953    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
954        self.detail.as_ref()
955    }
956
957    /// Return the numeric public wire code for this diagnostic.
958    #[must_use]
959    pub const fn error_code(&self) -> ErrorCode {
960        ErrorCode::from_parts(self.code, self.detail)
961    }
962}
963
964impl fmt::Debug for Diagnostic {
965    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
966        write!(f, "{}@{}", self.error_code().raw(), self.origin.wire_code())
967    }
968}
969
970fn fmt_compact_code(f: &mut fmt::Formatter<'_>, raw: u16) -> fmt::Result {
971    write!(f, "{raw}")
972}
973
974#[cfg(test)]
975mod tests {
976    use super::{
977        Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorClass, ErrorCode, ErrorOrigin,
978        QueryProjectionCode, QueryReadAdmissionCode, SqlFeatureCode, SqlLoweringCode,
979        SqlWriteBoundaryCode,
980        registry::{DETAIL_ERROR_CODES, ORDERED_ERROR_CODES},
981    };
982
983    #[test]
984    fn diagnostic_from_code_uses_default_origin() {
985        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
986
987        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
988        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
989    }
990
991    #[test]
992    fn diagnostic_code_reports_broad_class() {
993        assert_eq!(
994            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
995            ErrorClass::Unsupported
996        );
997        assert_eq!(
998            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
999            ErrorClass::Unsupported
1000        );
1001        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
1002        assert_eq!(
1003            DiagnosticCode::StoreCorruption.class(),
1004            ErrorClass::Corruption
1005        );
1006    }
1007
1008    #[test]
1009    fn class_and_origin_wire_codes_round_trip() {
1010        for (class, raw) in [
1011            (ErrorClass::Query, 1),
1012            (ErrorClass::Corruption, 2),
1013            (ErrorClass::IncompatiblePersistedFormat, 3),
1014            (ErrorClass::NotFound, 4),
1015            (ErrorClass::Internal, 5),
1016            (ErrorClass::Conflict, 6),
1017            (ErrorClass::Unsupported, 7),
1018            (ErrorClass::InvariantViolation, 8),
1019        ] {
1020            assert_eq!(class.wire_code(), raw);
1021            assert_eq!(ErrorClass::from_wire_code(raw), Some(class));
1022            assert_eq!(format!("{class:?}"), raw.to_string());
1023        }
1024
1025        for (origin, raw) in [
1026            (ErrorOrigin::Cursor, 1),
1027            (ErrorOrigin::Executor, 2),
1028            (ErrorOrigin::Identity, 3),
1029            (ErrorOrigin::Index, 4),
1030            (ErrorOrigin::Interface, 5),
1031            (ErrorOrigin::Planner, 6),
1032            (ErrorOrigin::Query, 7),
1033            (ErrorOrigin::Recovery, 8),
1034            (ErrorOrigin::Response, 9),
1035            (ErrorOrigin::Runtime, 10),
1036            (ErrorOrigin::Serialize, 11),
1037            (ErrorOrigin::Store, 12),
1038        ] {
1039            assert_eq!(origin.wire_code(), raw);
1040            assert_eq!(ErrorOrigin::from_known_wire_code(raw), Some(origin));
1041            assert_eq!(ErrorOrigin::from_wire_code(raw), origin);
1042            assert_eq!(format!("{origin:?}"), raw.to_string());
1043        }
1044
1045        assert_eq!(ErrorClass::from_wire_code(0), None);
1046        assert_eq!(ErrorOrigin::from_known_wire_code(0), None);
1047        assert_eq!(ErrorOrigin::from_wire_code(0), ErrorOrigin::Runtime);
1048    }
1049
1050    #[test]
1051    fn public_error_codes_are_sequential() {
1052        let first = ORDERED_ERROR_CODES
1053            .first()
1054            .expect("public error-code registry is non-empty")
1055            .raw();
1056
1057        assert_eq!(first, 1);
1058
1059        for (index, code) in ORDERED_ERROR_CODES.iter().enumerate() {
1060            let expected = first + u16::try_from(index).expect("test error-code index fits u16");
1061            assert_eq!(code.raw(), expected);
1062            assert_eq!(ErrorCode::known(code.raw()), Some(*code));
1063            assert!(code.is_known());
1064        }
1065
1066        let last = ORDERED_ERROR_CODES
1067            .last()
1068            .expect("public error-code registry is non-empty")
1069            .raw();
1070
1071        assert_eq!(last, 268);
1072    }
1073
1074    #[test]
1075    fn all_public_error_codes_round_trip_through_diagnostic_parts() {
1076        let first = ORDERED_ERROR_CODES
1077            .first()
1078            .expect("public error-code registry is non-empty")
1079            .raw();
1080        let last = ORDERED_ERROR_CODES
1081            .last()
1082            .expect("public error-code registry is non-empty")
1083            .raw();
1084
1085        for raw in first..=last {
1086            let code = ErrorCode::from_raw(raw);
1087            let diagnostic_code = code.diagnostic_code();
1088            let diagnostic_detail = code.diagnostic_detail();
1089            let rebuilt = ErrorCode::from_parts(diagnostic_code, diagnostic_detail);
1090
1091            assert_eq!(rebuilt.raw(), raw);
1092
1093            let diagnostic = code.diagnostic(ErrorOrigin::Runtime);
1094
1095            assert_eq!(diagnostic.code(), diagnostic_code);
1096            assert_eq!(diagnostic.detail(), diagnostic_detail.as_ref());
1097            assert_eq!(diagnostic.error_code().raw(), raw);
1098        }
1099    }
1100
1101    #[test]
1102    fn invalid_raw_error_codes_fail_closed_to_runtime_internal() {
1103        for raw in [0, 269, u16::MAX] {
1104            let code = ErrorCode::from_raw(raw);
1105
1106            assert_eq!(ErrorCode::known(raw), None);
1107            assert!(!code.is_known());
1108            assert_eq!(code.diagnostic_code(), DiagnosticCode::RuntimeInternal);
1109            assert_eq!(code.diagnostic_detail(), None);
1110            assert_eq!(code.class(), ErrorClass::Internal);
1111
1112            let diagnostic = code.diagnostic(ErrorOrigin::Query);
1113
1114            assert_eq!(diagnostic.code(), DiagnosticCode::RuntimeInternal);
1115            assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
1116            assert_eq!(diagnostic.detail(), None);
1117            assert_eq!(diagnostic.error_code(), ErrorCode::RUNTIME_INTERNAL);
1118        }
1119    }
1120
1121    #[test]
1122    fn from_parts_requires_detail_to_match_broad_code() {
1123        let detail = Some(DiagnosticDetail::UnsupportedSqlFeature {
1124            feature: SqlFeatureCode::Join,
1125        });
1126
1127        assert_eq!(
1128            ErrorCode::from_parts(DiagnosticCode::QueryUnsupportedSqlFeature, detail),
1129            ErrorCode::SQL_FEATURE_JOIN
1130        );
1131        assert_eq!(
1132            ErrorCode::from_parts(DiagnosticCode::QueryPlan, detail),
1133            ErrorCode::QUERY_PLAN
1134        );
1135    }
1136
1137    #[test]
1138    fn detail_bearing_registry_entries_round_trip_directly() {
1139        assert!(!DETAIL_ERROR_CODES.is_empty());
1140
1141        for &(code, diagnostic_code, detail) in DETAIL_ERROR_CODES {
1142            assert_eq!(ErrorCode::from_parts(diagnostic_code, Some(detail)), code);
1143            assert_eq!(code.diagnostic_code(), diagnostic_code);
1144            assert_eq!(code.diagnostic_detail(), Some(detail));
1145            assert_eq!(detail.diagnostic_code(), diagnostic_code);
1146        }
1147    }
1148
1149    #[test]
1150    fn diagnostic_detail_reports_generated_broad_code() {
1151        let detail = DiagnosticDetail::UnsupportedSqlFeature {
1152            feature: SqlFeatureCode::Join,
1153        };
1154
1155        assert_eq!(
1156            detail.diagnostic_code(),
1157            DiagnosticCode::QueryUnsupportedSqlFeature
1158        );
1159        assert_eq!(format!("{detail:?}"), "65");
1160    }
1161
1162    #[test]
1163    fn public_error_codes_reconstruct_shifted_details() {
1164        assert_eq!(
1165            ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD.diagnostic_code(),
1166            DiagnosticCode::QueryUnknownAggregateTargetField
1167        );
1168        assert_eq!(
1169            ErrorCode::SQL_FEATURE_JOIN.diagnostic_detail(),
1170            Some(DiagnosticDetail::UnsupportedSqlFeature {
1171                feature: SqlFeatureCode::Join,
1172            })
1173        );
1174        assert_eq!(
1175            ErrorCode::QUERY_PROJECTION_NUMERIC_LITERAL_REQUIRED.diagnostic_detail(),
1176            Some(DiagnosticDetail::QueryProjection {
1177                reason: QueryProjectionCode::NumericLiteralRequired,
1178            })
1179        );
1180        assert_eq!(
1181            ErrorCode::QUERY_READ_PUBLIC_REQUIRES_LIMIT.diagnostic_detail(),
1182            Some(DiagnosticDetail::QueryReadAdmission {
1183                reason: QueryReadAdmissionCode::PublicQueryRequiresLimit,
1184            })
1185        );
1186        assert_eq!(
1187            ErrorCode::SQL_LOWERING_DISTINCT_ORDER_BY_PROJECTION.diagnostic_detail(),
1188            Some(DiagnosticDetail::SqlLowering {
1189                reason: SqlLoweringCode::DistinctOrderByProjection,
1190            })
1191        );
1192        assert_eq!(
1193            ErrorCode::SQL_WRITE_RETURNING_RESPONSE_TOO_LARGE.diagnostic_detail(),
1194            Some(DiagnosticDetail::SqlWriteBoundary {
1195                boundary: SqlWriteBoundaryCode::ReturningResponseTooLarge,
1196            })
1197        );
1198        assert_eq!(
1199            ErrorCode::SQL_WRITE_RETURNING_ROWS_TOO_MANY.diagnostic_detail(),
1200            Some(DiagnosticDetail::SqlWriteBoundary {
1201                boundary: SqlWriteBoundaryCode::ReturningRowsTooMany,
1202            })
1203        );
1204    }
1205}