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