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    SqlQueryEntityNotFound,
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    /// One indivisible scalar-page unit cannot fit in an otherwise empty page envelope.
549    PageUnitTooLarge,
550    /// Database access was attempted without an active request-execution scope.
551    RequestExecutionScopeRequired,
552    /// An explicit request root conflicts with the root already active for this request.
553    RequestExecutionRootMismatch,
554}
555
556impl fmt::Debug for RuntimeBoundaryCode {
557    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558        fmt_compact_code(f, *self as u16)
559    }
560}
561
562///
563/// SqlFeatureCode
564///
565/// Compact SQL feature identifier used by unsupported-feature diagnostics.
566/// Variant order is wire-order significant for public error-code offsets.
567///
568
569#[repr(u16)]
570#[derive(Clone, Copy, Eq, Hash, PartialEq)]
571pub enum SqlFeatureCode {
572    AggregateFilterClause,
573    AlterStatementBeyondAlterTable,
574    AlterTableAddColumnDuplicateDefault,
575    AlterTableAddColumnModifiers,
576    AlterTableAddStatementBeyondAddColumn,
577    AlterTableAlterColumnDropUnsupportedAction,
578    AlterTableAlterColumnModifiers,
579    AlterTableAlterColumnSetUnsupportedAction,
580    AlterTableAlterColumnUnsupportedAction,
581    AlterTableAlterStatementBeyondAlterColumn,
582    AlterTableDropColumnIfExistsSyntax,
583    AlterTableDropColumnModifiers,
584    AlterTableDropStatementBeyondDropColumn,
585    AlterTableRenameColumnMissingTo,
586    AlterTableRenameColumnModifiers,
587    AlterTableRenameStatementBeyondRenameColumn,
588    AlterTableUnsupportedOperation,
589    ColumnAlias,
590    CreateIndexIfNotExistsSyntax,
591    CreateIndexKeyOrderingModifiers,
592    CreateIndexModifiers,
593    CreateStatementBeyondCreateIndex,
594    DescribeModifier,
595    DdlSchemaVersionDuplicateExpectedClause,
596    DdlSchemaVersionDuplicateSetClause,
597    DropIndexModifiers,
598    DropIndexIfExistsSyntax,
599    DropStatementBeyondDropIndex,
600    ExpressionIndexUnsupportedFunction,
601    Having,
602    Insert,
603    Join,
604    LikePatternBeyondTrailingPrefix,
605    LowerFieldPredicateUnsupported,
606    MultiStatementSql,
607    NestedAggregateInput,
608    NestedProjectionFunctionInArithmetic,
609    OrderByUnsupportedForm,
610    Other,
611    PredicateStartsWithFirstArgument,
612    QuotedIdentifiers,
613    ReturningUnsupportedShape,
614    ScalarFunctionExpressionPosition,
615    ScaleTakingNumericFunctionExpressionPosition,
616    SearchedCaseGroupedOrderBy,
617    ShowColumnsModifiers,
618    ShowEntitiesModifiers,
619    ShowIndexesModifiers,
620    ShowMemoryModifiers,
621    ShowStoresModifiers,
622    ShowUnsupportedCommand,
623    SimpleCaseExpression,
624    StandaloneLiteralProjectionItem,
625    SupportedGroupedOrderByExpressionFamily,
626    SupportedOrderByExpressionFamily,
627    UnionIntersectExcept,
628    UnsupportedFunctionNamespace,
629    Update,
630    UpperFieldPredicateUnsupported,
631    WindowFunction,
632    With,
633    NumericScaleFunctionArguments,
634    OrderByFieldNotOrderable,
635    ShowConstraintsModifiers,
636    AlterTableAddConstraintBeyondCheck,
637    AlterTableAddConstraintModifiers,
638    AlterTableDropConstraintIfExistsSyntax,
639    AlterTableDropConstraintModifiers,
640    AlterTableValidateBeyondConstraint,
641    AlterTableValidateConstraintModifiers,
642    ShowRelationsModifiers,
643}
644
645impl fmt::Debug for SqlFeatureCode {
646    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647        fmt_compact_code(f, *self as u16)
648    }
649}
650
651///
652/// SqlLoweringCode
653///
654/// Compact SQL lowering rejection identifier used after parsing succeeds but
655/// before a statement becomes canonical query intent.
656/// Variant order is wire-order significant for public error-code offsets.
657///
658
659#[repr(u16)]
660#[derive(Clone, Copy, Eq, Hash, PartialEq)]
661pub enum SqlLoweringCode {
662    EntityMismatch,
663    SelectProjectionShape,
664    SelectDistinct,
665    DistinctOrderByProjection,
666    GlobalAggregateProjection,
667    GlobalAggregateGroupBy,
668    SelectGroupByShape,
669    GroupedProjectionExplicitListRequired,
670    GroupedProjectionAggregateRequired,
671    GroupedProjectionNonGroupField,
672    GroupedProjectionScalarAfterAggregate,
673    HavingRequiresGroupBy,
674    SelectHavingShape,
675    AggregateInputExpressions,
676    WhereExpressionShape,
677    ParameterPlacement,
678    SqlDdlExecutionUnsupported,
679}
680
681impl fmt::Debug for SqlLoweringCode {
682    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
683        fmt_compact_code(f, *self as u16)
684    }
685}
686
687///
688/// SqlSurfaceMismatchCode
689///
690/// Compact SQL endpoint surface mismatch identifier.
691/// Variant order is wire-order significant for public error-code offsets.
692///
693
694#[repr(u16)]
695#[derive(Clone, Copy, Eq, Hash, PartialEq)]
696pub enum SqlSurfaceMismatchCode {
697    QueryRejectsInsert,
698    QueryRejectsUpdate,
699    QueryRejectsDelete,
700    MutationRejectsSelect,
701    MutationRejectsExplain,
702    MutationRejectsDescribe,
703    MutationRejectsShowIndexes,
704    MutationRejectsShowColumns,
705    MutationRejectsShowEntities,
706    MutationRejectsShowStores,
707    MutationRejectsShowMemory,
708    MutationRequiresExplicitUpdateIntent,
709    MutationRejectsShowConstraints,
710    MutationRejectsShowRelations,
711}
712
713impl fmt::Debug for SqlSurfaceMismatchCode {
714    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
715        fmt_compact_code(f, *self as u16)
716    }
717}
718
719///
720/// SqlWriteBoundaryCode
721///
722/// Compact SQL write fail-closed boundary identifier.
723/// Variant order is wire-order significant for public error-code offsets.
724///
725
726#[repr(u16)]
727#[derive(Clone, Copy, Eq, Hash, PartialEq)]
728pub enum SqlWriteBoundaryCode {
729    PrimaryKeyLiteralShape,
730    PrimaryKeyLiteralIncompatible,
731    MissingPrimaryKey,
732    MissingRequiredFields,
733    ExplicitManagedField,
734    ExplicitGeneratedField,
735    InsertSelectRequiresScalar,
736    InsertSelectAggregateProjection,
737    InsertSelectWidthMismatch,
738    UpdatePrimaryKeyMutation,
739    InvalidFieldLiteral,
740    UnknownReturningField,
741    DuplicateReturningField,
742    UpdateMissingWherePredicate,
743    WriteOrderByUnsupportedShape,
744    ReturningResponseTooLarge,
745    ReturningRowsTooMany,
746    StagedRowsTooMany,
747    InsertDefaultRequiredField,
748    UpdateDefaultRequiredField,
749    UpdateDefaultDatabaseOwnedField,
750    ExactUpdateAssertionRequired,
751    ExactUpdateAssertionTooHigh,
752    ExactUpdateAffectedRowsExceeded,
753    ExactUpdateWindowUnsupported,
754    ExactUpdateScanBudgetExceeded,
755    ResumableUpdateWindowUnsupported,
756    ResumableUpdateReturningUnsupported,
757    ResumableUpdateRequiresJournaledStore,
758    ResumableUpdateAssignedFieldHasGlobalConstraint,
759    ResumableUpdateScopeDependsOnAssignedField,
760    ResumableUpdateScopeDependencyUnknown,
761    ResumableUpdateContinuationMalformed,
762    ResumableUpdateContinuationTargetMismatch,
763    ResumableUpdateContinuationSchemaMismatch,
764    ResumableUpdateContinuationScopeMismatch,
765    ResumableUpdateContinuationPatchMismatch,
766    ResumableUpdateContinuationBatchPolicyMismatch,
767    ResumableUpdateSingleRowResourceExceeded,
768    ResumableUpdateManagedFieldHasGlobalConstraint,
769    ResumableUpdateContinuationOperationMismatch,
770}
771
772impl fmt::Debug for SqlWriteBoundaryCode {
773    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774        fmt_compact_code(f, *self as u16)
775    }
776}
777
778///
779/// SchemaDdlAdmissionCode
780///
781/// Compact SQL DDL admission rejection reason.
782/// Variant order is wire-order significant for public error-code offsets.
783///
784
785#[repr(u16)]
786#[derive(Clone, Copy, Eq, Hash, PartialEq)]
787pub enum SchemaDdlAdmissionCode {
788    MissingExpectedSchemaVersion,
789    MissingNextSchemaVersion,
790    StaleExpectedSchemaVersion,
791    InvalidExpectedSchemaVersion,
792    InvalidNextSchemaVersion,
793    AcceptedSchemaChangeWithoutVersionBump,
794    EmptyVersionBump,
795    VersionGap,
796    VersionRollback,
797    FingerprintMethodMismatch,
798    UnsupportedTransitionClass,
799    PhysicalRunnerMissing,
800    ValidationFailed,
801    PublicationRaceLost,
802    InvalidAddColumnDefault,
803    InvalidAlterColumnDefault,
804    GeneratedIndexDropRejected,
805    SchemaRewriteRequiresMigration,
806    SchemaTransitionBudgetExceeded,
807    GeneratedFieldDefaultChangeRejected,
808    GeneratedFieldNullabilityChangeRejected,
809    RowLayoutVersionExhausted,
810}
811
812impl fmt::Debug for SchemaDdlAdmissionCode {
813    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
814        fmt_compact_code(f, *self as u16)
815    }
816}
817
818/// Machine-readable source-migration rejection or lifecycle finding.
819#[repr(u16)]
820#[derive(Clone, Copy, Eq, Hash, PartialEq)]
821pub enum SchemaMigrationCode {
822    Unadopted,
823    MissingMigration,
824    VersionGap,
825    Downgrade,
826    EmptyEntityVersionBump,
827    DuplicateEntityTransition,
828    StaleAcceptedHead,
829    PlanChanged,
830    DuplicateRenameSource,
831    DuplicateRenameTarget,
832    UnknownFromObject,
833    UnknownToObject,
834    KindMismatch,
835    IdentityConflict,
836    IncompleteRenameCoverage,
837    UnexplainedSchemaDifference,
838    UnsupportedTransform,
839    TransformFinding,
840    UniqueIndexFinding,
841    RelationFinding,
842    ConstraintFinding,
843    PhysicalRunnerMissing,
844    MigrationInProgress,
845    AbortTooLate,
846    ProgressCorrupt,
847    CandidateMismatch,
848    PublicationRaceLost,
849}
850
851impl SchemaMigrationCode {
852    /// Return the broad diagnostic category for this migration result.
853    #[must_use]
854    pub const fn diagnostic_code(self) -> DiagnosticCode {
855        match self {
856            Self::StaleAcceptedHead
857            | Self::PlanChanged
858            | Self::IdentityConflict
859            | Self::MigrationInProgress
860            | Self::AbortTooLate
861            | Self::PublicationRaceLost => DiagnosticCode::RuntimeConflict,
862            Self::ProgressCorrupt | Self::CandidateMismatch => DiagnosticCode::RuntimeCorruption,
863            Self::Unadopted
864            | Self::MissingMigration
865            | Self::VersionGap
866            | Self::Downgrade
867            | Self::EmptyEntityVersionBump
868            | Self::DuplicateEntityTransition
869            | Self::DuplicateRenameSource
870            | Self::DuplicateRenameTarget
871            | Self::UnknownFromObject
872            | Self::UnknownToObject
873            | Self::KindMismatch
874            | Self::IncompleteRenameCoverage
875            | Self::UnexplainedSchemaDifference
876            | Self::UnsupportedTransform
877            | Self::TransformFinding
878            | Self::UniqueIndexFinding
879            | Self::RelationFinding
880            | Self::ConstraintFinding
881            | Self::PhysicalRunnerMissing => DiagnosticCode::RuntimeUnsupported,
882        }
883    }
884}
885
886impl fmt::Debug for SchemaMigrationCode {
887    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
888        fmt_compact_code(f, *self as u16)
889    }
890}
891
892///
893/// DiagnosticDetail
894///
895/// Small structured diagnostic payload for callers and CLI rendering.
896///
897
898#[remain::sorted]
899#[derive(Clone, Copy, Eq, PartialEq)]
900pub enum DiagnosticDetail {
901    QueryKind { kind: QueryErrorKind },
902    QueryProjection { reason: QueryProjectionCode },
903    QueryReadAdmission { reason: QueryReadAdmissionCode },
904    QueryResultShape { reason: QueryResultShapeCode },
905    RuntimeBoundary { boundary: RuntimeBoundaryCode },
906    RuntimeKind { kind: RuntimeErrorKind },
907    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
908    SchemaMigration { reason: SchemaMigrationCode },
909    SqlLowering { reason: SqlLoweringCode },
910    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
911    SqlWriteBoundary { boundary: SqlWriteBoundaryCode },
912    UnsupportedSqlFeature { feature: SqlFeatureCode },
913}
914
915impl fmt::Debug for DiagnosticDetail {
916    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
917        fmt_compact_code(
918            f,
919            ErrorCode::from_parts(self.diagnostic_code(), Some(*self)).raw(),
920        )
921    }
922}
923
924///
925/// Diagnostic
926///
927/// Compact public diagnostic payload.
928///
929
930#[derive(Clone, Eq, PartialEq)]
931pub struct Diagnostic {
932    code: DiagnosticCode,
933    origin: ErrorOrigin,
934    detail: Option<DiagnosticDetail>,
935}
936
937impl Diagnostic {
938    /// Build a compact diagnostic from a code and optional structured detail.
939    #[must_use]
940    pub const fn new(
941        code: DiagnosticCode,
942        origin: ErrorOrigin,
943        detail: Option<DiagnosticDetail>,
944    ) -> Self {
945        Self {
946            code,
947            origin,
948            detail,
949        }
950    }
951
952    /// Build a compact diagnostic using the code's default origin.
953    #[must_use]
954    pub const fn from_code(code: DiagnosticCode) -> Self {
955        Self::new(code, code.origin(), None)
956    }
957
958    /// Return the stable diagnostic code.
959    #[must_use]
960    pub const fn code(&self) -> DiagnosticCode {
961        self.code
962    }
963
964    /// Return the diagnostic class.
965    #[must_use]
966    pub const fn class(&self) -> ErrorClass {
967        self.code.class()
968    }
969
970    /// Return the subsystem origin.
971    #[must_use]
972    pub const fn origin(&self) -> ErrorOrigin {
973        self.origin
974    }
975
976    /// Return structured diagnostic detail, when available.
977    #[must_use]
978    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
979        self.detail.as_ref()
980    }
981
982    /// Return the numeric public wire code for this diagnostic.
983    #[must_use]
984    pub const fn error_code(&self) -> ErrorCode {
985        ErrorCode::from_parts(self.code, self.detail)
986    }
987}
988
989impl fmt::Debug for Diagnostic {
990    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
991        write!(f, "{}@{}", self.error_code().raw(), self.origin.wire_code())
992    }
993}
994
995fn fmt_compact_code(f: &mut fmt::Formatter<'_>, raw: u16) -> fmt::Result {
996    write!(f, "{raw}")
997}
998
999#[cfg(test)]
1000mod tests {
1001    use super::{
1002        Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorClass, ErrorCode, ErrorOrigin,
1003        QueryProjectionCode, QueryReadAdmissionCode, SqlFeatureCode, SqlLoweringCode,
1004        SqlWriteBoundaryCode,
1005        registry::{DETAIL_ERROR_CODES, ORDERED_ERROR_CODES},
1006    };
1007
1008    #[test]
1009    fn diagnostic_from_code_uses_default_origin() {
1010        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
1011
1012        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
1013        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
1014    }
1015
1016    #[test]
1017    fn diagnostic_code_reports_broad_class() {
1018        assert_eq!(
1019            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
1020            ErrorClass::Unsupported
1021        );
1022        assert_eq!(
1023            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
1024            ErrorClass::Unsupported
1025        );
1026        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
1027        assert_eq!(
1028            DiagnosticCode::StoreCorruption.class(),
1029            ErrorClass::Corruption
1030        );
1031    }
1032
1033    #[test]
1034    fn class_and_origin_wire_codes_round_trip() {
1035        for (class, raw) in [
1036            (ErrorClass::Query, 1),
1037            (ErrorClass::Corruption, 2),
1038            (ErrorClass::IncompatiblePersistedFormat, 3),
1039            (ErrorClass::NotFound, 4),
1040            (ErrorClass::Internal, 5),
1041            (ErrorClass::Conflict, 6),
1042            (ErrorClass::Unsupported, 7),
1043            (ErrorClass::InvariantViolation, 8),
1044        ] {
1045            assert_eq!(class.wire_code(), raw);
1046            assert_eq!(ErrorClass::from_wire_code(raw), Some(class));
1047            assert_eq!(format!("{class:?}"), raw.to_string());
1048        }
1049
1050        for (origin, raw) in [
1051            (ErrorOrigin::Cursor, 1),
1052            (ErrorOrigin::Executor, 2),
1053            (ErrorOrigin::Identity, 3),
1054            (ErrorOrigin::Index, 4),
1055            (ErrorOrigin::Interface, 5),
1056            (ErrorOrigin::Planner, 6),
1057            (ErrorOrigin::Query, 7),
1058            (ErrorOrigin::Recovery, 8),
1059            (ErrorOrigin::Response, 9),
1060            (ErrorOrigin::Runtime, 10),
1061            (ErrorOrigin::Serialize, 11),
1062            (ErrorOrigin::Store, 12),
1063        ] {
1064            assert_eq!(origin.wire_code(), raw);
1065            assert_eq!(ErrorOrigin::from_known_wire_code(raw), Some(origin));
1066            assert_eq!(ErrorOrigin::from_wire_code(raw), origin);
1067            assert_eq!(format!("{origin:?}"), raw.to_string());
1068        }
1069
1070        assert_eq!(ErrorClass::from_wire_code(0), None);
1071        assert_eq!(ErrorOrigin::from_known_wire_code(0), None);
1072        assert_eq!(ErrorOrigin::from_wire_code(0), ErrorOrigin::Runtime);
1073    }
1074
1075    #[test]
1076    fn public_error_codes_are_sequential() {
1077        let first = ORDERED_ERROR_CODES
1078            .first()
1079            .expect("public error-code registry is non-empty")
1080            .raw();
1081
1082        assert_eq!(first, 1);
1083
1084        for (index, code) in ORDERED_ERROR_CODES.iter().enumerate() {
1085            let expected = first + u16::try_from(index).expect("test error-code index fits u16");
1086            assert_eq!(code.raw(), expected);
1087            assert_eq!(ErrorCode::known(code.raw()), Some(*code));
1088            assert!(code.is_known());
1089        }
1090
1091        let last = ORDERED_ERROR_CODES
1092            .last()
1093            .expect("public error-code registry is non-empty")
1094            .raw();
1095
1096        assert_eq!(last, 278);
1097    }
1098
1099    #[test]
1100    fn all_public_error_codes_round_trip_through_diagnostic_parts() {
1101        let first = ORDERED_ERROR_CODES
1102            .first()
1103            .expect("public error-code registry is non-empty")
1104            .raw();
1105        let last = ORDERED_ERROR_CODES
1106            .last()
1107            .expect("public error-code registry is non-empty")
1108            .raw();
1109
1110        for raw in first..=last {
1111            let code = ErrorCode::from_raw(raw);
1112            let diagnostic_code = code.diagnostic_code();
1113            let diagnostic_detail = code.diagnostic_detail();
1114            let rebuilt = ErrorCode::from_parts(diagnostic_code, diagnostic_detail);
1115
1116            assert_eq!(rebuilt.raw(), raw);
1117
1118            let diagnostic = code.diagnostic(ErrorOrigin::Runtime);
1119
1120            assert_eq!(diagnostic.code(), diagnostic_code);
1121            assert_eq!(diagnostic.detail(), diagnostic_detail.as_ref());
1122            assert_eq!(diagnostic.error_code().raw(), raw);
1123        }
1124    }
1125
1126    #[test]
1127    fn invalid_raw_error_codes_fail_closed_to_runtime_internal() {
1128        let first_unknown = ORDERED_ERROR_CODES
1129            .last()
1130            .expect("public error-code registry is non-empty")
1131            .raw()
1132            .checked_add(1)
1133            .expect("public error-code registry retains an unknown successor");
1134
1135        for raw in [0, first_unknown, u16::MAX] {
1136            let code = ErrorCode::from_raw(raw);
1137
1138            assert_eq!(ErrorCode::known(raw), None);
1139            assert!(!code.is_known());
1140            assert_eq!(code.diagnostic_code(), DiagnosticCode::RuntimeInternal);
1141            assert_eq!(code.diagnostic_detail(), None);
1142            assert_eq!(code.class(), ErrorClass::Internal);
1143
1144            let diagnostic = code.diagnostic(ErrorOrigin::Query);
1145
1146            assert_eq!(diagnostic.code(), DiagnosticCode::RuntimeInternal);
1147            assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
1148            assert_eq!(diagnostic.detail(), None);
1149            assert_eq!(diagnostic.error_code(), ErrorCode::RUNTIME_INTERNAL);
1150        }
1151    }
1152
1153    #[test]
1154    fn from_parts_requires_detail_to_match_broad_code() {
1155        let detail = Some(DiagnosticDetail::UnsupportedSqlFeature {
1156            feature: SqlFeatureCode::Join,
1157        });
1158
1159        assert_eq!(
1160            ErrorCode::from_parts(DiagnosticCode::QueryUnsupportedSqlFeature, detail),
1161            ErrorCode::SQL_FEATURE_JOIN
1162        );
1163        assert_eq!(
1164            ErrorCode::from_parts(DiagnosticCode::QueryPlan, detail),
1165            ErrorCode::QUERY_PLAN
1166        );
1167    }
1168
1169    #[test]
1170    fn detail_bearing_registry_entries_round_trip_directly() {
1171        assert!(!DETAIL_ERROR_CODES.is_empty());
1172
1173        for &(code, diagnostic_code, detail) in DETAIL_ERROR_CODES {
1174            assert_eq!(ErrorCode::from_parts(diagnostic_code, Some(detail)), code);
1175            assert_eq!(code.diagnostic_code(), diagnostic_code);
1176            assert_eq!(code.diagnostic_detail(), Some(detail));
1177            assert_eq!(detail.diagnostic_code(), diagnostic_code);
1178        }
1179    }
1180
1181    #[test]
1182    fn diagnostic_detail_reports_generated_broad_code() {
1183        let detail = DiagnosticDetail::UnsupportedSqlFeature {
1184            feature: SqlFeatureCode::Join,
1185        };
1186
1187        assert_eq!(
1188            detail.diagnostic_code(),
1189            DiagnosticCode::QueryUnsupportedSqlFeature
1190        );
1191        assert_eq!(format!("{detail:?}"), "65");
1192    }
1193
1194    #[test]
1195    fn public_error_codes_reconstruct_shifted_details() {
1196        assert_eq!(
1197            ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD.diagnostic_code(),
1198            DiagnosticCode::QueryUnknownAggregateTargetField
1199        );
1200        assert_eq!(
1201            ErrorCode::SQL_FEATURE_JOIN.diagnostic_detail(),
1202            Some(DiagnosticDetail::UnsupportedSqlFeature {
1203                feature: SqlFeatureCode::Join,
1204            })
1205        );
1206        assert_eq!(
1207            ErrorCode::QUERY_PROJECTION_NUMERIC_LITERAL_REQUIRED.diagnostic_detail(),
1208            Some(DiagnosticDetail::QueryProjection {
1209                reason: QueryProjectionCode::NumericLiteralRequired,
1210            })
1211        );
1212        assert_eq!(
1213            ErrorCode::QUERY_READ_PUBLIC_REQUIRES_LIMIT.diagnostic_detail(),
1214            Some(DiagnosticDetail::QueryReadAdmission {
1215                reason: QueryReadAdmissionCode::PublicQueryRequiresLimit,
1216            })
1217        );
1218        assert_eq!(
1219            ErrorCode::SQL_LOWERING_DISTINCT_ORDER_BY_PROJECTION.diagnostic_detail(),
1220            Some(DiagnosticDetail::SqlLowering {
1221                reason: SqlLoweringCode::DistinctOrderByProjection,
1222            })
1223        );
1224        assert_eq!(
1225            ErrorCode::SQL_WRITE_RETURNING_RESPONSE_TOO_LARGE.diagnostic_detail(),
1226            Some(DiagnosticDetail::SqlWriteBoundary {
1227                boundary: SqlWriteBoundaryCode::ReturningResponseTooLarge,
1228            })
1229        );
1230        assert_eq!(
1231            ErrorCode::SQL_WRITE_RETURNING_ROWS_TOO_MANY.diagnostic_detail(),
1232            Some(DiagnosticDetail::SqlWriteBoundary {
1233                boundary: SqlWriteBoundaryCode::ReturningRowsTooMany,
1234            })
1235        );
1236    }
1237}