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    BindingCount,
665    BindingFamily,
666    BindingLimit,
667}
668
669impl fmt::Debug for SqlLoweringCode {
670    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
671        fmt_compact_code(f, *self as u16)
672    }
673}
674
675///
676/// SqlSurfaceMismatchCode
677///
678/// Compact SQL endpoint surface mismatch identifier.
679/// Variant order is wire-order significant for public error-code offsets.
680///
681
682#[repr(u16)]
683#[derive(Clone, Copy, Eq, Hash, PartialEq)]
684pub enum SqlSurfaceMismatchCode {
685    QueryRejectsInsert,
686    QueryRejectsUpdate,
687    QueryRejectsDelete,
688    MutationRejectsSelect,
689    MutationRejectsExplain,
690    MutationRejectsDescribe,
691    MutationRejectsShowIndexes,
692    MutationRejectsShowColumns,
693    MutationRejectsShowEntities,
694    MutationRejectsShowStores,
695    MutationRejectsShowMemory,
696    MutationRequiresExplicitUpdateIntent,
697    MutationRejectsShowConstraints,
698    MutationRejectsShowRelations,
699}
700
701impl fmt::Debug for SqlSurfaceMismatchCode {
702    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
703        fmt_compact_code(f, *self as u16)
704    }
705}
706
707///
708/// SqlWriteBoundaryCode
709///
710/// Compact SQL write fail-closed boundary identifier.
711/// Variant order is wire-order significant for public error-code offsets.
712///
713
714#[repr(u16)]
715#[derive(Clone, Copy, Eq, Hash, PartialEq)]
716pub enum SqlWriteBoundaryCode {
717    PrimaryKeyLiteralIncompatible,
718    MissingPrimaryKey,
719    MissingRequiredFields,
720    ExplicitManagedField,
721    ExplicitGeneratedField,
722    InsertSelectRequiresScalar,
723    InsertSelectAggregateProjection,
724    InsertSelectWidthMismatch,
725    UpdatePrimaryKeyMutation,
726    InvalidFieldLiteral,
727    UnknownReturningField,
728    DuplicateReturningField,
729    UpdateMissingWherePredicate,
730    WriteOrderByUnsupportedShape,
731    ReturningResponseTooLarge,
732    ReturningRowsTooMany,
733    StagedRowsTooMany,
734    InsertDefaultRequiredField,
735    UpdateDefaultRequiredField,
736    UpdateDefaultDatabaseOwnedField,
737    ExactUpdateAssertionRequired,
738    ExactUpdateAssertionTooHigh,
739    ExactUpdateAffectedRowsExceeded,
740    ExactUpdateWindowUnsupported,
741    ExactUpdateScanBudgetExceeded,
742    ResumableUpdateWindowUnsupported,
743    ResumableUpdateReturningUnsupported,
744    ResumableUpdateRequiresJournaledStore,
745    ResumableUpdateAssignedFieldHasGlobalConstraint,
746    ResumableUpdateScopeDependsOnAssignedField,
747    ResumableUpdateScopeDependencyUnknown,
748    ResumableUpdateContinuationMalformed,
749    ResumableUpdateContinuationTargetMismatch,
750    ResumableUpdateContinuationSchemaMismatch,
751    ResumableUpdateContinuationScopeMismatch,
752    ResumableUpdateContinuationPatchMismatch,
753    ResumableUpdateContinuationBatchPolicyMismatch,
754    ResumableUpdateManagedFieldHasGlobalConstraint,
755}
756
757impl fmt::Debug for SqlWriteBoundaryCode {
758    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759        fmt_compact_code(f, *self as u16)
760    }
761}
762
763///
764/// SchemaDdlAdmissionCode
765///
766/// Compact SQL DDL admission rejection reason.
767/// Variant order is wire-order significant for public error-code offsets.
768///
769
770#[repr(u16)]
771#[derive(Clone, Copy, Eq, Hash, PartialEq)]
772pub enum SchemaDdlAdmissionCode {
773    MissingExpectedSchemaVersion,
774    MissingNextSchemaVersion,
775    StaleExpectedSchemaVersion,
776    InvalidExpectedSchemaVersion,
777    InvalidNextSchemaVersion,
778    AcceptedSchemaChangeWithoutVersionBump,
779    EmptyVersionBump,
780    VersionGap,
781    VersionRollback,
782    FingerprintMethodMismatch,
783    UnsupportedTransitionClass,
784    PhysicalRunnerMissing,
785    ValidationFailed,
786    PublicationRaceLost,
787    InvalidAddColumnDefault,
788    InvalidAlterColumnDefault,
789    GeneratedIndexDropRejected,
790    SchemaRewriteRequiresMigration,
791    SchemaTransitionBudgetExceeded,
792    GeneratedFieldDefaultChangeRejected,
793    GeneratedFieldNullabilityChangeRejected,
794    RowLayoutVersionExhausted,
795}
796
797impl fmt::Debug for SchemaDdlAdmissionCode {
798    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
799        fmt_compact_code(f, *self as u16)
800    }
801}
802
803/// Machine-readable source-migration rejection or lifecycle finding.
804#[repr(u16)]
805#[derive(Clone, Copy, Eq, Hash, PartialEq)]
806pub enum SchemaMigrationCode {
807    Unadopted,
808    MissingMigration,
809    VersionGap,
810    Downgrade,
811    EmptyEntityVersionBump,
812    StaleAcceptedHead,
813    PlanChanged,
814    UnknownFromObject,
815    UnknownToObject,
816    KindMismatch,
817    IdentityConflict,
818    UnexplainedSchemaDifference,
819    UnsupportedTransform,
820    PhysicalRunnerMissing,
821    MigrationInProgress,
822    AbortTooLate,
823    ProgressCorrupt,
824    CandidateMismatch,
825    PublicationRaceLost,
826}
827
828impl SchemaMigrationCode {
829    /// Return the broad diagnostic category for this migration result.
830    #[must_use]
831    pub const fn diagnostic_code(self) -> DiagnosticCode {
832        match self {
833            Self::StaleAcceptedHead
834            | Self::PlanChanged
835            | Self::IdentityConflict
836            | Self::MigrationInProgress
837            | Self::AbortTooLate
838            | Self::PublicationRaceLost => DiagnosticCode::RuntimeConflict,
839            Self::ProgressCorrupt | Self::CandidateMismatch => DiagnosticCode::RuntimeCorruption,
840            Self::Unadopted
841            | Self::MissingMigration
842            | Self::VersionGap
843            | Self::Downgrade
844            | Self::EmptyEntityVersionBump
845            | Self::UnknownFromObject
846            | Self::UnknownToObject
847            | Self::KindMismatch
848            | Self::UnexplainedSchemaDifference
849            | Self::UnsupportedTransform
850            | Self::PhysicalRunnerMissing => DiagnosticCode::RuntimeUnsupported,
851        }
852    }
853}
854
855impl fmt::Debug for SchemaMigrationCode {
856    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
857        fmt_compact_code(f, *self as u16)
858    }
859}
860
861///
862/// DiagnosticDetail
863///
864/// Small structured diagnostic payload for callers and CLI rendering.
865///
866
867#[remain::sorted]
868#[derive(Clone, Copy, Eq, PartialEq)]
869pub enum DiagnosticDetail {
870    QueryKind { kind: QueryErrorKind },
871    QueryProjection { reason: QueryProjectionCode },
872    QueryReadAdmission { reason: QueryReadAdmissionCode },
873    RuntimeBoundary { boundary: RuntimeBoundaryCode },
874    RuntimeKind { kind: RuntimeErrorKind },
875    SchemaDdlAdmission { reason: SchemaDdlAdmissionCode },
876    SchemaMigration { reason: SchemaMigrationCode },
877    SqlLowering { reason: SqlLoweringCode },
878    SqlSurfaceMismatch { mismatch: SqlSurfaceMismatchCode },
879    SqlWriteBoundary { boundary: SqlWriteBoundaryCode },
880    UnsupportedSqlFeature { feature: SqlFeatureCode },
881}
882
883impl fmt::Debug for DiagnosticDetail {
884    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885        fmt_compact_code(
886            f,
887            ErrorCode::from_parts(self.diagnostic_code(), Some(*self)).raw(),
888        )
889    }
890}
891
892///
893/// Diagnostic
894///
895/// Compact public diagnostic payload.
896///
897
898#[derive(Clone, Eq, PartialEq)]
899pub struct Diagnostic {
900    code: DiagnosticCode,
901    origin: ErrorOrigin,
902    detail: Option<DiagnosticDetail>,
903}
904
905impl Diagnostic {
906    /// Build a compact diagnostic from a code and optional structured detail.
907    #[must_use]
908    pub const fn new(
909        code: DiagnosticCode,
910        origin: ErrorOrigin,
911        detail: Option<DiagnosticDetail>,
912    ) -> Self {
913        Self {
914            code,
915            origin,
916            detail,
917        }
918    }
919
920    /// Build a compact diagnostic using the code's default origin.
921    #[must_use]
922    pub const fn from_code(code: DiagnosticCode) -> Self {
923        Self::new(code, code.origin(), None)
924    }
925
926    /// Return the stable diagnostic code.
927    #[must_use]
928    pub const fn code(&self) -> DiagnosticCode {
929        self.code
930    }
931
932    /// Return the diagnostic class.
933    #[must_use]
934    pub const fn class(&self) -> ErrorClass {
935        self.code.class()
936    }
937
938    /// Return the subsystem origin.
939    #[must_use]
940    pub const fn origin(&self) -> ErrorOrigin {
941        self.origin
942    }
943
944    /// Return structured diagnostic detail, when available.
945    #[must_use]
946    pub const fn detail(&self) -> Option<&DiagnosticDetail> {
947        self.detail.as_ref()
948    }
949
950    /// Return the numeric public wire code for this diagnostic.
951    #[must_use]
952    pub const fn error_code(&self) -> ErrorCode {
953        ErrorCode::from_parts(self.code, self.detail)
954    }
955}
956
957impl fmt::Debug for Diagnostic {
958    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
959        write!(f, "{}@{}", self.error_code().raw(), self.origin.wire_code())
960    }
961}
962
963fn fmt_compact_code(f: &mut fmt::Formatter<'_>, raw: u16) -> fmt::Result {
964    write!(f, "{raw}")
965}
966
967#[cfg(test)]
968mod tests {
969    use super::{
970        Diagnostic, DiagnosticCode, DiagnosticDetail, ErrorClass, ErrorCode, ErrorOrigin,
971        QueryProjectionCode, QueryReadAdmissionCode, SqlFeatureCode, SqlLoweringCode,
972        SqlWriteBoundaryCode,
973        registry::{DETAIL_ERROR_CODES, ORDERED_ERROR_CODES},
974    };
975
976    #[test]
977    fn diagnostic_from_code_uses_default_origin() {
978        let diagnostic = Diagnostic::from_code(DiagnosticCode::QueryPlan);
979
980        assert_eq!(diagnostic.code(), DiagnosticCode::QueryPlan);
981        assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
982    }
983
984    #[test]
985    fn diagnostic_code_reports_broad_class() {
986        assert_eq!(
987            DiagnosticCode::QueryUnsupportedSqlFeature.class(),
988            ErrorClass::Unsupported
989        );
990        assert_eq!(
991            DiagnosticCode::QuerySqlSurfaceMismatch.class(),
992            ErrorClass::Unsupported
993        );
994        assert_eq!(DiagnosticCode::QueryPlan.class(), ErrorClass::Query);
995        assert_eq!(
996            DiagnosticCode::StoreCorruption.class(),
997            ErrorClass::Corruption
998        );
999    }
1000
1001    #[test]
1002    fn class_and_origin_wire_codes_round_trip() {
1003        for (class, raw) in [
1004            (ErrorClass::Query, 1),
1005            (ErrorClass::Corruption, 2),
1006            (ErrorClass::IncompatiblePersistedFormat, 3),
1007            (ErrorClass::NotFound, 4),
1008            (ErrorClass::Internal, 5),
1009            (ErrorClass::Conflict, 6),
1010            (ErrorClass::Unsupported, 7),
1011            (ErrorClass::InvariantViolation, 8),
1012        ] {
1013            assert_eq!(class.wire_code(), raw);
1014            assert_eq!(ErrorClass::from_wire_code(raw), Some(class));
1015            assert_eq!(format!("{class:?}"), raw.to_string());
1016        }
1017
1018        for (origin, raw) in [
1019            (ErrorOrigin::Cursor, 1),
1020            (ErrorOrigin::Executor, 2),
1021            (ErrorOrigin::Identity, 3),
1022            (ErrorOrigin::Index, 4),
1023            (ErrorOrigin::Interface, 5),
1024            (ErrorOrigin::Planner, 6),
1025            (ErrorOrigin::Query, 7),
1026            (ErrorOrigin::Recovery, 8),
1027            (ErrorOrigin::Response, 9),
1028            (ErrorOrigin::Runtime, 10),
1029            (ErrorOrigin::Serialize, 11),
1030            (ErrorOrigin::Store, 12),
1031        ] {
1032            assert_eq!(origin.wire_code(), raw);
1033            assert_eq!(ErrorOrigin::from_known_wire_code(raw), Some(origin));
1034            assert_eq!(ErrorOrigin::from_wire_code(raw), origin);
1035            assert_eq!(format!("{origin:?}"), raw.to_string());
1036        }
1037
1038        assert_eq!(ErrorClass::from_wire_code(0), None);
1039        assert_eq!(ErrorOrigin::from_known_wire_code(0), None);
1040        assert_eq!(ErrorOrigin::from_wire_code(0), ErrorOrigin::Runtime);
1041    }
1042
1043    #[test]
1044    fn public_error_codes_are_sequential() {
1045        let first = ORDERED_ERROR_CODES
1046            .first()
1047            .expect("public error-code registry is non-empty")
1048            .raw();
1049
1050        assert_eq!(first, 1);
1051
1052        for (index, code) in ORDERED_ERROR_CODES.iter().enumerate() {
1053            let expected = first + u16::try_from(index).expect("test error-code index fits u16");
1054            assert_eq!(code.raw(), expected);
1055            assert_eq!(ErrorCode::known(code.raw()), Some(*code));
1056            assert!(code.is_known());
1057        }
1058
1059        let last = ORDERED_ERROR_CODES
1060            .last()
1061            .expect("public error-code registry is non-empty")
1062            .raw();
1063
1064        assert_eq!(last, 267);
1065    }
1066
1067    #[test]
1068    fn all_public_error_codes_round_trip_through_diagnostic_parts() {
1069        let first = ORDERED_ERROR_CODES
1070            .first()
1071            .expect("public error-code registry is non-empty")
1072            .raw();
1073        let last = ORDERED_ERROR_CODES
1074            .last()
1075            .expect("public error-code registry is non-empty")
1076            .raw();
1077
1078        for raw in first..=last {
1079            let code = ErrorCode::from_raw(raw);
1080            let diagnostic_code = code.diagnostic_code();
1081            let diagnostic_detail = code.diagnostic_detail();
1082            let rebuilt = ErrorCode::from_parts(diagnostic_code, diagnostic_detail);
1083
1084            assert_eq!(rebuilt.raw(), raw);
1085
1086            let diagnostic = code.diagnostic(ErrorOrigin::Runtime);
1087
1088            assert_eq!(diagnostic.code(), diagnostic_code);
1089            assert_eq!(diagnostic.detail(), diagnostic_detail.as_ref());
1090            assert_eq!(diagnostic.error_code().raw(), raw);
1091        }
1092    }
1093
1094    #[test]
1095    fn invalid_raw_error_codes_fail_closed_to_runtime_internal() {
1096        let first_unknown = ORDERED_ERROR_CODES
1097            .last()
1098            .expect("public error-code registry is non-empty")
1099            .raw()
1100            .checked_add(1)
1101            .expect("public error-code registry retains an unknown successor");
1102
1103        for raw in [0, first_unknown, u16::MAX] {
1104            let code = ErrorCode::from_raw(raw);
1105
1106            assert_eq!(ErrorCode::known(raw), None);
1107            assert!(!code.is_known());
1108            assert_eq!(code.diagnostic_code(), DiagnosticCode::RuntimeInternal);
1109            assert_eq!(code.diagnostic_detail(), None);
1110            assert_eq!(code.class(), ErrorClass::Internal);
1111
1112            let diagnostic = code.diagnostic(ErrorOrigin::Query);
1113
1114            assert_eq!(diagnostic.code(), DiagnosticCode::RuntimeInternal);
1115            assert_eq!(diagnostic.origin(), ErrorOrigin::Query);
1116            assert_eq!(diagnostic.detail(), None);
1117            assert_eq!(diagnostic.error_code(), ErrorCode::RUNTIME_INTERNAL);
1118        }
1119    }
1120
1121    #[test]
1122    fn from_parts_requires_detail_to_match_broad_code() {
1123        let detail = Some(DiagnosticDetail::UnsupportedSqlFeature {
1124            feature: SqlFeatureCode::Join,
1125        });
1126
1127        assert_eq!(
1128            ErrorCode::from_parts(DiagnosticCode::QueryUnsupportedSqlFeature, detail),
1129            ErrorCode::SQL_FEATURE_JOIN
1130        );
1131        assert_eq!(
1132            ErrorCode::from_parts(DiagnosticCode::QueryPlan, detail),
1133            ErrorCode::QUERY_PLAN
1134        );
1135    }
1136
1137    #[test]
1138    fn detail_bearing_registry_entries_round_trip_directly() {
1139        assert!(!DETAIL_ERROR_CODES.is_empty());
1140
1141        for &(code, diagnostic_code, detail) in DETAIL_ERROR_CODES {
1142            assert_eq!(ErrorCode::from_parts(diagnostic_code, Some(detail)), code);
1143            assert_eq!(code.diagnostic_code(), diagnostic_code);
1144            assert_eq!(code.diagnostic_detail(), Some(detail));
1145            assert_eq!(detail.diagnostic_code(), diagnostic_code);
1146        }
1147    }
1148
1149    #[test]
1150    fn diagnostic_detail_reports_generated_broad_code() {
1151        let detail = DiagnosticDetail::UnsupportedSqlFeature {
1152            feature: SqlFeatureCode::Join,
1153        };
1154
1155        assert_eq!(
1156            detail.diagnostic_code(),
1157            DiagnosticCode::QueryUnsupportedSqlFeature
1158        );
1159        assert_eq!(format!("{detail:?}"), "61");
1160    }
1161
1162    #[test]
1163    fn public_error_codes_reconstruct_shifted_details() {
1164        assert_eq!(
1165            ErrorCode::QUERY_UNKNOWN_AGGREGATE_TARGET_FIELD.diagnostic_code(),
1166            DiagnosticCode::QueryUnknownAggregateTargetField
1167        );
1168        assert_eq!(
1169            ErrorCode::SQL_FEATURE_JOIN.diagnostic_detail(),
1170            Some(DiagnosticDetail::UnsupportedSqlFeature {
1171                feature: SqlFeatureCode::Join,
1172            })
1173        );
1174        assert_eq!(
1175            ErrorCode::QUERY_PROJECTION_NUMERIC_LITERAL_REQUIRED.diagnostic_detail(),
1176            Some(DiagnosticDetail::QueryProjection {
1177                reason: QueryProjectionCode::NumericLiteralRequired,
1178            })
1179        );
1180        assert_eq!(
1181            ErrorCode::QUERY_READ_PUBLIC_REQUIRES_LIMIT.diagnostic_detail(),
1182            Some(DiagnosticDetail::QueryReadAdmission {
1183                reason: QueryReadAdmissionCode::PublicQueryRequiresLimit,
1184            })
1185        );
1186        assert_eq!(
1187            ErrorCode::SQL_LOWERING_DISTINCT_ORDER_BY_PROJECTION.diagnostic_detail(),
1188            Some(DiagnosticDetail::SqlLowering {
1189                reason: SqlLoweringCode::DistinctOrderByProjection,
1190            })
1191        );
1192        assert_eq!(
1193            ErrorCode::SQL_WRITE_RETURNING_RESPONSE_TOO_LARGE.diagnostic_detail(),
1194            Some(DiagnosticDetail::SqlWriteBoundary {
1195                boundary: SqlWriteBoundaryCode::ReturningResponseTooLarge,
1196            })
1197        );
1198        assert_eq!(
1199            ErrorCode::SQL_WRITE_RETURNING_ROWS_TOO_MANY.diagnostic_detail(),
1200            Some(DiagnosticDetail::SqlWriteBoundary {
1201                boundary: SqlWriteBoundaryCode::ReturningRowsTooMany,
1202            })
1203        );
1204    }
1205}