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