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