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