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