Skip to main content

icydb_diagnostic_code/
lib.rs

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