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