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