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