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