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