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