Skip to main content

icydb_diagnostic_code/
lib.rs

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