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