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    /// Logical explain describes a request before any cursor boundary.
434    ExplainDoesNotAcceptCursor,
435}
436
437impl fmt::Debug for QueryReadAdmissionCode {
438    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439        fmt_compact_code(f, *self as u16)
440    }
441}
442
443///
444/// RuntimeErrorKind
445///
446/// Public runtime error category.
447///
448
449#[repr(u16)]
450#[derive(Clone, Copy, Eq, Hash, PartialEq)]
451pub enum RuntimeErrorKind {
452    Corruption,
453    IncompatiblePersistedFormat,
454    InvariantViolation,
455    Conflict,
456    NotFound,
457    Unsupported,
458    Internal,
459}
460
461impl fmt::Debug for RuntimeErrorKind {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        fmt_compact_code(f, *self as u16)
464    }
465}
466
467///
468/// RuntimeBoundaryCode
469///
470/// Compact public-runtime boundary identifier.
471/// Variant order is wire-order significant for public error-code offsets.
472///
473
474#[repr(u16)]
475#[derive(Clone, Copy, Eq, Hash, PartialEq)]
476pub enum RuntimeBoundaryCode {
477    SqlSurfaceControllerRequired,
478    SchemaSurfaceControllerRequired,
479    SqlQueryNoConfiguredEntities,
480    SqlQueryEntityNotFound,
481    SqlDdlTargetRequired,
482    SqlDdlEntityNotConfigured,
483    SqlIntrospectionDisabled,
484    /// A complete accepted mutation omitted a required field.
485    MutationRequiredFieldMissing,
486    /// A logical write would move an accepted managed timestamp backward.
487    MutationManagedTimestampRegression,
488    /// A persisted row's stamp falls outside the accepted layout window.
489    PersistedRowLayoutOutsideAcceptedWindow,
490    /// A persisted row's physical slot count disagrees with its layout stamp.
491    PersistedRowSlotCountMismatch,
492    /// A generated field would collide with an accepted DDL-owned slot.
493    GeneratedFieldAfterDdlField,
494    /// A journaled mutation cannot reserve a representable post-commit revision.
495    JournalMutationRevisionExhausted,
496    /// A final canonical after-image violates one accepted row constraint or gate.
497    ConstraintViolation,
498    /// Accepted row-constraint metadata or its compiled program is inconsistent.
499    AcceptedRowConstraintProgramCorrupt,
500    /// A write conflicts with one incomplete accepted constraint activation.
501    ConstraintActivationWriteBlocked,
502    /// A live generated constraint activation no longer matches its proposal.
503    GeneratedConstraintActivationStale,
504    /// A caller explicitly authored a field owned by accepted database policy.
505    MutationDatabaseOwnedFieldExplicit,
506    /// A mixed structural mutation batch contained no operations.
507    MutationBatchEmpty,
508    /// A mixed structural mutation batch exceeded its operation-count bound.
509    MutationBatchTooManyItems,
510    /// A mixed structural mutation batch exceeded its staged-byte bound.
511    MutationBatchStagedBytesExceeded,
512    /// A mixed structural mutation result exceeded its encoded response bound.
513    MutationBatchResultBytesExceeded,
514    /// A mixed structural mutation batch crossed an accepted store boundary.
515    MutationBatchStoreMismatch,
516    /// A mixed structural mutation batch exceeded its distinct-entity bound.
517    MutationBatchTooManyEntities,
518    /// More than one mixed structural operation targeted the same accepted key.
519    MutationBatchDuplicateKey,
520    /// An operational report or reset endpoint requires a controller caller.
521    OperationalSurfaceControllerRequired,
522    /// An exact-key batch exceeded its admitted input item count.
523    ExactKeyBatchTooManyItems,
524    /// An exact-key batch exceeded its admitted encoded input-key bytes.
525    ExactKeyBatchInputBytesExceeded,
526    /// An exact-key batch exceeded its admitted distinct stored-row bytes.
527    ExactKeyBatchStoredBytesExceeded,
528    /// An exact-key batch exceeded its admitted logical result bytes.
529    ExactKeyBatchResultBytesExceeded,
530    /// One charged execution resource exceeded its absolute safety ceiling.
531    ExecutionBudgetExceeded,
532    /// One indivisible scalar-page unit cannot fit in an otherwise empty page envelope.
533    PageUnitTooLarge,
534    /// Database access was attempted without an active request-execution scope.
535    RequestExecutionScopeRequired,
536    /// An explicit request root conflicts with the root already active for this request.
537    RequestExecutionRootMismatch,
538    /// A successful generated SQL query reply exceeds the deployed IC query-response limit.
539    SqlQueryReplyBytesExceeded,
540    QueryExplainOutputExceeded,
541    /// A derived logical explain access tree exceeds the diagnostic depth limit.
542    QueryExplainDepthExceeded,
543    /// Startup recovery remains incomplete and ordinary database work must retry later.
544    DatabaseStartupRecoveryPending,
545    /// An application guard denied access to the generated SQL read surface.
546    SqlSurfacePolicyDenied,
547    /// An application guard denied access to the generated accepted-schema surface.
548    SchemaSurfacePolicyDenied,
549    /// A structural mutation batch exceeded its canonical prepared-commit work bound.
550    MutationBatchCommitWorkExceeded,
551    /// Retained journal debt leaves insufficient bounded convergence capacity.
552    ConvergenceBacklogPressure,
553}
554
555impl fmt::Debug for RuntimeBoundaryCode {
556    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557        fmt_compact_code(f, *self as u16)
558    }
559}
560
561///
562/// SqlFeatureCode
563///
564/// Compact SQL feature identifier used by unsupported-feature diagnostics.
565/// Variant order is wire-order significant for public error-code offsets.
566///
567
568#[repr(u16)]
569#[derive(Clone, Copy, Eq, Hash, PartialEq)]
570pub enum SqlFeatureCode {
571    AggregateFilterClause,
572    AlterStatementBeyondAlterTable,
573    AlterTableAddColumnDuplicateDefault,
574    AlterTableAddColumnModifiers,
575    AlterTableAddStatementBeyondAddColumn,
576    AlterTableAlterColumnDropUnsupportedAction,
577    AlterTableAlterColumnModifiers,
578    AlterTableAlterColumnSetUnsupportedAction,
579    AlterTableAlterColumnUnsupportedAction,
580    AlterTableAlterStatementBeyondAlterColumn,
581    AlterTableDropColumnIfExistsSyntax,
582    AlterTableDropColumnModifiers,
583    AlterTableDropStatementBeyondDropColumn,
584    AlterTableRenameColumnMissingTo,
585    AlterTableRenameColumnModifiers,
586    AlterTableRenameStatementBeyondRenameColumn,
587    AlterTableUnsupportedOperation,
588    ColumnAlias,
589    CreateIndexIfNotExistsSyntax,
590    CreateIndexKeyOrderingModifiers,
591    CreateIndexModifiers,
592    CreateStatementBeyondCreateIndex,
593    DescribeModifier,
594    DdlSchemaVersionDuplicateExpectedClause,
595    DdlSchemaVersionDuplicateSetClause,
596    DropIndexModifiers,
597    DropIndexIfExistsSyntax,
598    DropStatementBeyondDropIndex,
599    ExpressionIndexUnsupportedFunction,
600    Having,
601    Insert,
602    Join,
603    LikePatternBeyondTrailingPrefix,
604    LowerFieldPredicateUnsupported,
605    MultiStatementSql,
606    NestedAggregateInput,
607    NestedProjectionFunctionInArithmetic,
608    OrderByUnsupportedForm,
609    Other,
610    PredicateStartsWithFirstArgument,
611    QuotedIdentifiers,
612    ReturningUnsupportedShape,
613    ScalarFunctionExpressionPosition,
614    ScaleTakingNumericFunctionExpressionPosition,
615    ShowColumnsModifiers,
616    ShowEntitiesModifiers,
617    ShowIndexesModifiers,
618    ShowMemoryModifiers,
619    ShowStoresModifiers,
620    ShowUnsupportedCommand,
621    SimpleCaseExpression,
622    StandaloneLiteralProjectionItem,
623    UnionIntersectExcept,
624    UnsupportedFunctionNamespace,
625    Update,
626    UpperFieldPredicateUnsupported,
627    WindowFunction,
628    With,
629    NumericScaleFunctionArguments,
630    OrderByFieldNotOrderable,
631    ShowConstraintsModifiers,
632    AlterTableAddConstraintBeyondCheck,
633    AlterTableAddConstraintModifiers,
634    AlterTableDropConstraintIfExistsSyntax,
635    AlterTableDropConstraintModifiers,
636    AlterTableValidateBeyondConstraint,
637    AlterTableValidateConstraintModifiers,
638    ShowRelationsModifiers,
639}
640
641impl fmt::Debug for SqlFeatureCode {
642    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643        fmt_compact_code(f, *self as u16)
644    }
645}
646
647///
648/// SqlLoweringCode
649///
650/// Compact SQL lowering rejection identifier used after parsing succeeds but
651/// before a statement becomes canonical query intent.
652/// Variant order is wire-order significant for public error-code offsets.
653///
654
655#[repr(u16)]
656#[derive(Clone, Copy, Eq, Hash, PartialEq)]
657pub enum SqlLoweringCode {
658    EntityMismatch,
659    SelectProjectionShape,
660    SelectDistinct,
661    DistinctOrderByProjection,
662    GlobalAggregateProjection,
663    GlobalAggregateGroupBy,
664    SelectGroupByShape,
665    GroupedProjectionExplicitListRequired,
666    GroupedProjectionAggregateRequired,
667    GroupedProjectionNonGroupField,
668    GroupedProjectionScalarAfterAggregate,
669    HavingRequiresGroupBy,
670    SelectHavingShape,
671    AggregateInputExpressions,
672    WhereExpressionShape,
673    ParameterPlacement,
674    SqlDdlExecutionUnsupported,
675    BindingCount,
676    BindingFamily,
677    BindingLimit,
678}
679
680impl fmt::Debug for SqlLoweringCode {
681    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
682        fmt_compact_code(f, *self as u16)
683    }
684}
685
686///
687/// SqlSurfaceMismatchCode
688///
689/// Compact SQL endpoint surface mismatch identifier.
690/// Variant order is wire-order significant for public error-code offsets.
691///
692
693#[repr(u16)]
694#[derive(Clone, Copy, Eq, Hash, PartialEq)]
695pub enum SqlSurfaceMismatchCode {
696    QueryRejectsInsert,
697    QueryRejectsUpdate,
698    QueryRejectsDelete,
699    MutationRejectsSelect,
700    MutationRejectsExplain,
701    MutationRejectsDescribe,
702    MutationRejectsShowIndexes,
703    MutationRejectsShowColumns,
704    MutationRejectsShowEntities,
705    MutationRejectsShowStores,
706    MutationRejectsShowMemory,
707    MutationRequiresExplicitUpdateIntent,
708    MutationRejectsShowConstraints,
709    MutationRejectsShowRelations,
710}
711
712impl fmt::Debug for SqlSurfaceMismatchCode {
713    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
714        fmt_compact_code(f, *self as u16)
715    }
716}
717
718///
719/// SqlWriteBoundaryCode
720///
721/// Compact SQL write fail-closed boundary identifier.
722/// Variant order is wire-order significant for public error-code offsets.
723///
724
725#[repr(u16)]
726#[derive(Clone, Copy, Eq, Hash, PartialEq)]
727pub enum SqlWriteBoundaryCode {
728    PrimaryKeyLiteralIncompatible,
729    MissingPrimaryKey,
730    MissingRequiredFields,
731    ExplicitManagedField,
732    ExplicitGeneratedField,
733    InsertSelectRequiresScalar,
734    InsertSelectAggregateProjection,
735    InsertSelectWidthMismatch,
736    UpdatePrimaryKeyMutation,
737    InvalidFieldLiteral,
738    UnknownReturningField,
739    DuplicateReturningField,
740    UpdateMissingWherePredicate,
741    WriteOrderByUnsupportedShape,
742    ReturningResponseTooLarge,
743    ReturningRowsTooMany,
744    StagedRowsTooMany,
745    InsertDefaultRequiredField,
746    UpdateDefaultRequiredField,
747    UpdateDefaultDatabaseOwnedField,
748    ExactUpdateAssertionRequired,
749    ExactUpdateAssertionTooHigh,
750    ExactUpdateAffectedRowsExceeded,
751    ExactUpdateWindowUnsupported,
752    ExactUpdateScanBudgetExceeded,
753    ResumableUpdateWindowUnsupported,
754    ResumableUpdateReturningUnsupported,
755    ResumableUpdateRequiresJournaledStore,
756    ResumableUpdateAssignedFieldHasGlobalConstraint,
757    ResumableUpdateScopeDependsOnAssignedField,
758    ResumableUpdateScopeDependencyUnknown,
759    ResumableUpdateContinuationMalformed,
760    ResumableUpdateContinuationTargetMismatch,
761    ResumableUpdateContinuationSchemaMismatch,
762    ResumableUpdateContinuationScopeMismatch,
763    ResumableUpdateContinuationPatchMismatch,
764    ResumableUpdateContinuationBatchPolicyMismatch,
765    ResumableUpdateManagedFieldHasGlobalConstraint,
766}
767
768impl fmt::Debug for SqlWriteBoundaryCode {
769    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
770        fmt_compact_code(f, *self as u16)
771    }
772}
773
774///
775/// SchemaDdlAdmissionCode
776///
777/// Compact SQL DDL admission rejection reason.
778/// Variant order is wire-order significant for public error-code offsets.
779///
780
781#[repr(u16)]
782#[derive(Clone, Copy, Eq, Hash, PartialEq)]
783pub enum SchemaDdlAdmissionCode {
784    MissingExpectedSchemaVersion,
785    MissingNextSchemaVersion,
786    StaleExpectedSchemaVersion,
787    InvalidExpectedSchemaVersion,
788    InvalidNextSchemaVersion,
789    AcceptedSchemaChangeWithoutVersionBump,
790    EmptyVersionBump,
791    VersionGap,
792    VersionRollback,
793    FingerprintMethodMismatch,
794    UnsupportedTransitionClass,
795    PhysicalRunnerMissing,
796    ValidationFailed,
797    PublicationRaceLost,
798    InvalidAddColumnDefault,
799    InvalidAlterColumnDefault,
800    GeneratedIndexDropRejected,
801    SchemaRewriteRequiresMigration,
802    SchemaTransitionBudgetExceeded,
803    GeneratedFieldDefaultChangeRejected,
804    GeneratedFieldNullabilityChangeRejected,
805    RowLayoutVersionExhausted,
806}
807
808impl fmt::Debug for SchemaDdlAdmissionCode {
809    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
810        fmt_compact_code(f, *self as u16)
811    }
812}
813
814/// Machine-readable source-migration rejection or lifecycle finding.
815#[repr(u16)]
816#[derive(Clone, Copy, Eq, Hash, PartialEq)]
817pub enum SchemaMigrationCode {
818    Unadopted,
819    MissingMigration,
820    VersionGap,
821    Downgrade,
822    EmptyEntityVersionBump,
823    StaleAcceptedHead,
824    PlanChanged,
825    UnknownFromObject,
826    UnknownToObject,
827    KindMismatch,
828    IdentityConflict,
829    UnexplainedSchemaDifference,
830    UnsupportedTransform,
831    PhysicalRunnerMissing,
832    MigrationInProgress,
833    AbortTooLate,
834    ProgressCorrupt,
835    CandidateMismatch,
836    PublicationRaceLost,
837}
838
839impl SchemaMigrationCode {
840    /// Return the broad diagnostic category for this migration result.
841    #[must_use]
842    pub const fn diagnostic_code(self) -> DiagnosticCode {
843        match self {
844            Self::StaleAcceptedHead
845            | Self::PlanChanged
846            | Self::IdentityConflict
847            | Self::MigrationInProgress
848            | Self::AbortTooLate
849            | Self::PublicationRaceLost => DiagnosticCode::RuntimeConflict,
850            Self::ProgressCorrupt | Self::CandidateMismatch => DiagnosticCode::RuntimeCorruption,
851            Self::Unadopted
852            | Self::MissingMigration
853            | Self::VersionGap
854            | Self::Downgrade
855            | Self::EmptyEntityVersionBump
856            | Self::UnknownFromObject
857            | Self::UnknownToObject
858            | Self::KindMismatch
859            | Self::UnexplainedSchemaDifference
860            | Self::UnsupportedTransform
861            | Self::PhysicalRunnerMissing => DiagnosticCode::RuntimeUnsupported,
862        }
863    }
864}
865
866impl fmt::Debug for SchemaMigrationCode {
867    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
868        fmt_compact_code(f, *self as u16)
869    }
870}
871
872///
873/// DiagnosticDetail
874///
875/// Small structured diagnostic payload for callers and CLI rendering.
876///
877
878#[remain::sorted]
879#[derive(Clone, Copy, Eq, PartialEq)]
880pub enum DiagnosticDetail {
881    QueryKind { kind: QueryErrorKind },
882    QueryProjection { reason: QueryProjectionCode },
883    QueryReadAdmission { reason: QueryReadAdmissionCode },
884    RuntimeBoundary { boundary: RuntimeBoundaryCode },
885    RuntimeKind { kind: RuntimeErrorKind },
886    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
887    SchemaMigration { reason: SchemaMigrationCode },
888    SqlLowering { reason: SqlLoweringCode },
889    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
890    SqlWriteBoundary { boundary: SqlWriteBoundaryCode },
891    UnsupportedSqlFeature { feature: SqlFeatureCode },
892}
893
894impl fmt::Debug for DiagnosticDetail {
895    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
896        fmt_compact_code(
897            f,
898            ErrorCode::from_parts(self.diagnostic_code(), Some(*self)).raw(),
899        )
900    }
901}
902
903///
904/// Diagnostic
905///
906/// Compact public diagnostic payload.
907///
908
909#[derive(Clone, Eq, PartialEq)]
910pub struct Diagnostic {
911    code: DiagnosticCode,
912    origin: ErrorOrigin,
913    detail: Option<DiagnosticDetail>,
914}
915
916impl Diagnostic {
917    /// Build a compact diagnostic from a code and optional structured detail.
918    #[must_use]
919    pub const fn new(
920        code: DiagnosticCode,
921        origin: ErrorOrigin,
922        detail: Option<DiagnosticDetail>,
923    ) -> Self {
924        Self {
925            code,
926            origin,
927            detail,
928        }
929    }
930
931    /// Build a compact diagnostic using the code's default origin.
932    #[must_use]
933    pub const fn from_code(code: DiagnosticCode) -> Self {
934        Self::new(code, code.origin(), None)
935    }
936
937    /// Return the stable diagnostic code.
938    #[must_use]
939    pub const fn code(&self) -> DiagnosticCode {
940        self.code
941    }
942
943    /// Return the diagnostic class.
944    #[must_use]
945    pub const fn class(&self) -> ErrorClass {
946        self.code.class()
947    }
948
949    /// Return the subsystem origin.
950    #[must_use]
951    pub const fn origin(&self) -> ErrorOrigin {
952        self.origin
953    }
954
955    /// Return structured diagnostic detail, when available.
956    #[must_use]
957    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
958        self.detail.as_ref()
959    }
960
961    /// Return the numeric public wire code for this diagnostic.
962    #[must_use]
963    pub const fn error_code(&self) -> ErrorCode {
964        ErrorCode::from_parts(self.code, self.detail)
965    }
966}
967
968impl fmt::Debug for Diagnostic {
969    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
970        write!(f, "{}@{}", self.error_code().raw(), self.origin.wire_code())
971    }
972}
973
974fn fmt_compact_code(f: &mut fmt::Formatter<'_>, raw: u16) -> fmt::Result {
975    write!(f, "{raw}")
976}
977
978#[cfg(test)]
979mod tests {
980    use super::{
981        Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorClass, ErrorCode, ErrorOrigin,
982        QueryProjectionCode, QueryReadAdmissionCode, SqlFeatureCode, SqlLoweringCode,
983        SqlWriteBoundaryCode,
984        registry::{DETAIL_ERROR_CODES, ORDERED_ERROR_CODES},
985    };
986
987    #[test]
988    fn diagnostic_from_code_uses_default_origin() {
989        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
990
991        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
992        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
993    }
994
995    #[test]
996    fn diagnostic_code_reports_broad_class() {
997        assert_eq!(
998            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
999            ErrorClass::Unsupported
1000        );
1001        assert_eq!(
1002            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
1003            ErrorClass::Unsupported
1004        );
1005        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
1006        assert_eq!(
1007            DiagnosticCode::StoreCorruption.class(),
1008            ErrorClass::Corruption
1009        );
1010    }
1011
1012    #[test]
1013    fn class_and_origin_wire_codes_round_trip() {
1014        for (class, raw) in [
1015            (ErrorClass::Query, 1),
1016            (ErrorClass::Corruption, 2),
1017            (ErrorClass::IncompatiblePersistedFormat, 3),
1018            (ErrorClass::NotFound, 4),
1019            (ErrorClass::Internal, 5),
1020            (ErrorClass::Conflict, 6),
1021            (ErrorClass::Unsupported, 7),
1022            (ErrorClass::InvariantViolation, 8),
1023        ] {
1024            assert_eq!(class.wire_code(), raw);
1025            assert_eq!(ErrorClass::from_wire_code(raw), Some(class));
1026            assert_eq!(format!("{class:?}"), raw.to_string());
1027        }
1028
1029        for (origin, raw) in [
1030            (ErrorOrigin::Cursor, 1),
1031            (ErrorOrigin::Executor, 2),
1032            (ErrorOrigin::Identity, 3),
1033            (ErrorOrigin::Index, 4),
1034            (ErrorOrigin::Interface, 5),
1035            (ErrorOrigin::Planner, 6),
1036            (ErrorOrigin::Query, 7),
1037            (ErrorOrigin::Recovery, 8),
1038            (ErrorOrigin::Response, 9),
1039            (ErrorOrigin::Runtime, 10),
1040            (ErrorOrigin::Serialize, 11),
1041            (ErrorOrigin::Store, 12),
1042        ] {
1043            assert_eq!(origin.wire_code(), raw);
1044            assert_eq!(ErrorOrigin::from_known_wire_code(raw), Some(origin));
1045            assert_eq!(ErrorOrigin::from_wire_code(raw), origin);
1046            assert_eq!(format!("{origin:?}"), raw.to_string());
1047        }
1048
1049        assert_eq!(ErrorClass::from_wire_code(0), None);
1050        assert_eq!(ErrorOrigin::from_known_wire_code(0), None);
1051        assert_eq!(ErrorOrigin::from_wire_code(0), ErrorOrigin::Runtime);
1052    }
1053
1054    #[test]
1055    fn public_error_codes_are_sequential() {
1056        let first = ORDERED_ERROR_CODES
1057            .first()
1058            .expect("public error-code registry is non-empty")
1059            .raw();
1060
1061        assert_eq!(first, 1);
1062
1063        for (index, code) in ORDERED_ERROR_CODES.iter().enumerate() {
1064            let expected = first + u16::try_from(index).expect("test error-code index fits u16");
1065            assert_eq!(code.raw(), expected);
1066            assert_eq!(ErrorCode::known(code.raw()), Some(*code));
1067            assert!(code.is_known());
1068        }
1069
1070        let last = ORDERED_ERROR_CODES
1071            .last()
1072            .expect("public error-code registry is non-empty")
1073            .raw();
1074
1075        assert_eq!(last, 273);
1076    }
1077
1078    #[test]
1079    fn all_public_error_codes_round_trip_through_diagnostic_parts() {
1080        let first = ORDERED_ERROR_CODES
1081            .first()
1082            .expect("public error-code registry is non-empty")
1083            .raw();
1084        let last = ORDERED_ERROR_CODES
1085            .last()
1086            .expect("public error-code registry is non-empty")
1087            .raw();
1088
1089        for raw in first..=last {
1090            let code = ErrorCode::from_raw(raw);
1091            let diagnostic_code = code.diagnostic_code();
1092            let diagnostic_detail = code.diagnostic_detail();
1093            let rebuilt = ErrorCode::from_parts(diagnostic_code, diagnostic_detail);
1094
1095            assert_eq!(rebuilt.raw(), raw);
1096
1097            let diagnostic = code.diagnostic(ErrorOrigin::Runtime);
1098
1099            assert_eq!(diagnostic.code(), diagnostic_code);
1100            assert_eq!(diagnostic.detail(), diagnostic_detail.as_ref());
1101            assert_eq!(diagnostic.error_code().raw(), raw);
1102        }
1103    }
1104
1105    #[test]
1106    fn invalid_raw_error_codes_fail_closed_to_runtime_internal() {
1107        let first_unknown = ORDERED_ERROR_CODES
1108            .last()
1109            .expect("public error-code registry is non-empty")
1110            .raw()
1111            .checked_add(1)
1112            .expect("public error-code registry retains an unknown successor");
1113
1114        for raw in [0, first_unknown, u16::MAX] {
1115            let code = ErrorCode::from_raw(raw);
1116
1117            assert_eq!(ErrorCode::known(raw), None);
1118            assert!(!code.is_known());
1119            assert_eq!(code.diagnostic_code(), DiagnosticCode::RuntimeInternal);
1120            assert_eq!(code.diagnostic_detail(), None);
1121            assert_eq!(code.class(), ErrorClass::Internal);
1122
1123            let diagnostic = code.diagnostic(ErrorOrigin::Query);
1124
1125            assert_eq!(diagnostic.code(), DiagnosticCode::RuntimeInternal);
1126            assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
1127            assert_eq!(diagnostic.detail(), None);
1128            assert_eq!(diagnostic.error_code(), ErrorCode::RUNTIME_INTERNAL);
1129        }
1130    }
1131
1132    #[test]
1133    fn from_parts_requires_detail_to_match_broad_code() {
1134        let detail = Some(DiagnosticDetail::UnsupportedSqlFeature {
1135            feature: SqlFeatureCode::Join,
1136        });
1137
1138        assert_eq!(
1139            ErrorCode::from_parts(DiagnosticCode::QueryUnsupportedSqlFeature, detail),
1140            ErrorCode::SQL_FEATURE_JOIN
1141        );
1142        assert_eq!(
1143            ErrorCode::from_parts(DiagnosticCode::QueryPlan, detail),
1144            ErrorCode::QUERY_PLAN
1145        );
1146    }
1147
1148    #[test]
1149    fn detail_bearing_registry_entries_round_trip_directly() {
1150        assert!(!DETAIL_ERROR_CODES.is_empty());
1151
1152        for &(code, diagnostic_code, detail) in DETAIL_ERROR_CODES {
1153            assert_eq!(ErrorCode::from_parts(diagnostic_code, Some(detail)), code);
1154            assert_eq!(code.diagnostic_code(), diagnostic_code);
1155            assert_eq!(code.diagnostic_detail(), Some(detail));
1156            assert_eq!(detail.diagnostic_code(), diagnostic_code);
1157        }
1158    }
1159
1160    #[test]
1161    fn diagnostic_detail_reports_generated_broad_code() {
1162        let detail = DiagnosticDetail::UnsupportedSqlFeature {
1163            feature: SqlFeatureCode::Join,
1164        };
1165
1166        assert_eq!(
1167            detail.diagnostic_code(),
1168            DiagnosticCode::QueryUnsupportedSqlFeature
1169        );
1170        assert_eq!(format!("{detail:?}"), "61");
1171    }
1172
1173    #[test]
1174    fn public_error_codes_reconstruct_shifted_details() {
1175        assert_eq!(
1176            ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD.diagnostic_code(),
1177            DiagnosticCode::QueryUnknownAggregateTargetField
1178        );
1179        assert_eq!(
1180            ErrorCode::SQL_FEATURE_JOIN.diagnostic_detail(),
1181            Some(DiagnosticDetail::UnsupportedSqlFeature {
1182                feature: SqlFeatureCode::Join,
1183            })
1184        );
1185        assert_eq!(
1186            ErrorCode::QUERY_PROJECTION_NUMERIC_LITERAL_REQUIRED.diagnostic_detail(),
1187            Some(DiagnosticDetail::QueryProjection {
1188                reason: QueryProjectionCode::NumericLiteralRequired,
1189            })
1190        );
1191        assert_eq!(
1192            ErrorCode::QUERY_READ_PUBLIC_REQUIRES_LIMIT.diagnostic_detail(),
1193            Some(DiagnosticDetail::QueryReadAdmission {
1194                reason: QueryReadAdmissionCode::PublicQueryRequiresLimit,
1195            })
1196        );
1197        assert_eq!(
1198            ErrorCode::SQL_LOWERING_DISTINCT_ORDER_BY_PROJECTION.diagnostic_detail(),
1199            Some(DiagnosticDetail::SqlLowering {
1200                reason: SqlLoweringCode::DistinctOrderByProjection,
1201            })
1202        );
1203        assert_eq!(
1204            ErrorCode::SQL_WRITE_RETURNING_RESPONSE_TOO_LARGE.diagnostic_detail(),
1205            Some(DiagnosticDetail::SqlWriteBoundary {
1206                boundary: SqlWriteBoundaryCode::ReturningResponseTooLarge,
1207            })
1208        );
1209        assert_eq!(
1210            ErrorCode::SQL_WRITE_RETURNING_ROWS_TOO_MANY.diagnostic_detail(),
1211            Some(DiagnosticDetail::SqlWriteBoundary {
1212                boundary: SqlWriteBoundaryCode::ReturningRowsTooMany,
1213            })
1214        );
1215    }
1216}