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