Skip to main content

icydb_diagnostic_code/
fact.rs

1//! Module: fact
2//!
3//! Responsibility: production-safe numeric diagnostic-fact identities.
4//! Does not own: Candid records, rich labels, prose, or subsystem projections.
5//! Boundary: freezes the numeric vocabulary shared by public errors and host tooling.
6
7use std::fmt;
8
9/// Maximum number of numeric facts carried by one public error.
10pub const MAX_PUBLIC_DIAGNOSTIC_FACTS: usize = 80;
11
12macro_rules! define_fact_tag_registry {
13    ($($name:ident = $raw:literal;)+) => {
14        /// Stable semantic identity for one numeric public diagnostic fact.
15        #[derive(Clone, Copy, Eq, Hash, PartialEq)]
16        pub enum DiagnosticFactTag {
17            $(
18                #[doc = concat!("Public fact tag ", stringify!($raw), ".")]
19                $name,
20            )+
21        }
22
23        impl DiagnosticFactTag {
24            /// Return the fixed public wire value.
25            #[must_use]
26            pub const fn raw(self) -> u8 {
27                match self {
28                    $(Self::$name => $raw,)+
29                }
30            }
31
32            /// Recover a known tag from its public wire value.
33            #[must_use]
34            pub const fn known(raw: u8) -> Option<Self> {
35                match raw {
36                    $($raw => Some(Self::$name),)+
37                    _ => None,
38                }
39            }
40        }
41
42        #[cfg(test)]
43        const ORDERED_FACT_TAGS: &[DiagnosticFactTag] = &[
44            $(DiagnosticFactTag::$name,)+
45        ];
46    };
47}
48
49// This table is a public numeric registry. Append only within a released
50// major-version contract; do not reuse or reinterpret an assigned value.
51define_fact_tag_registry! {
52    AcceptedSchemaFingerprintMethod = 1;
53    AcceptedSchemaFingerprintHigh = 2;
54    AcceptedSchemaFingerprintLow = 3;
55    ExpectedFingerprintPrefix = 4;
56    ActualFingerprintPrefix = 5;
57    EntityTag = 6;
58    ExpectedEntityTag = 7;
59    ActualEntityTag = 8;
60    ConstraintId = 9;
61    FieldId = 10;
62    IndexId = 11;
63    RelationId = 12;
64    MutationOperation = 13;
65    RowOperation = 14;
66    BatchPosition = 15;
67    FirstBatchPosition = 16;
68    DuplicateBatchPosition = 17;
69    ClauseIndex = 18;
70    TermIndex = 19;
71    FirstTermIndex = 20;
72    DuplicateTermIndex = 21;
73    ProjectionIndex = 22;
74    GroupIndex = 23;
75    AggregateIndex = 24;
76    ArgumentIndex = 25;
77    BranchIndex = 26;
78    ComponentIndex = 27;
79    ParameterIndex = 28;
80    SourceSpanStart = 29;
81    SourceSpanEnd = 30;
82    Expected = 31;
83    Actual = 32;
84    Minimum = 33;
85    Maximum = 34;
86    Limit = 35;
87    ExpectedCount = 36;
88    ActualCount = 37;
89    ExpectedRevision = 38;
90    ActualRevision = 39;
91    CurrentRevision = 40;
92    RequestedRevision = 41;
93    ExpectedVersion = 42;
94    ActualVersion = 43;
95    CurrentVersion = 44;
96    RequestedVersion = 45;
97    ExpectedOffset = 46;
98    ActualOffset = 47;
99    ExpectedArity = 48;
100    ActualArity = 49;
101    ExpectedLength = 50;
102    ActualLength = 51;
103    ExpectedSlotCount = 52;
104    ActualSlotCount = 53;
105    RowLayout = 54;
106    HistoryFloor = 55;
107    CurrentLayout = 56;
108    PhysicalSlot = 57;
109    PhysicalGeneration = 58;
110    ExpectedMemoryId = 59;
111    ActualMemoryId = 60;
112    ConstraintKind = 61;
113    ConstraintContext = 62;
114    FieldKind = 63;
115    ValueKind = 64;
116    TypeFamily = 65;
117    FunctionKind = 66;
118    OperatorKind = 67;
119    AggregateKind = 68;
120    KeyNamespaceKind = 69;
121    ComponentKind = 70;
122    MismatchKind = 71;
123    DecodeReason = 72;
124    BudgetResource = 73;
125    MigrationPhase = 74;
126    DatabaseControlRecordKind = 75;
127    StateKind = 76;
128    PayloadComponent = 77;
129    ExpectedSignaturePrefix = 78;
130    ActualSignaturePrefix = 79;
131    FindingPosition = 80;
132    RootField = 81;
133    RecordMember = 82;
134    TupleElement = 83;
135    Newtype = 84;
136    EnumVariant = 85;
137    ListElement = 86;
138    SetElement = 87;
139    MapEntryKey = 88;
140    MapEntryValue = 89;
141}
142
143/// Compact reason carried by [`DiagnosticFactTag::DecodeReason`].
144///
145/// Values are global within that fact tag. They identify only bounded decode
146/// or validation categories and never retain rejected payload bytes.
147#[derive(Clone, Copy, Eq, Hash, PartialEq)]
148pub enum DiagnosticDecodeReason {
149    CursorEmpty,
150    CursorTooLong,
151    CursorOddLength,
152    CursorInvalidHex,
153    CursorGroupedDirectionMismatch,
154    CursorTokenEncode,
155    CursorTokenDecode,
156    RecoveryMarkerMagic,
157    RecoveryMarkerChecksum,
158    RecoveryMarkerState,
159}
160
161/// Compact operation carried by [`DiagnosticFactTag::MutationOperation`].
162#[derive(Clone, Copy, Eq, Hash, PartialEq)]
163pub enum DiagnosticMutationOperation {
164    Insert,
165    Replace,
166    Update,
167    Delete,
168}
169
170macro_rules! define_numeric_fact_value_registry {
171    (
172        $(#[$enum_meta:meta])*
173        pub enum $name:ident {
174            $($variant:ident = $raw:literal;)+
175        }
176    ) => {
177        $(#[$enum_meta])*
178        #[derive(Clone, Copy, Eq, Hash, PartialEq)]
179        pub enum $name {
180            $(
181                #[doc = concat!("Compact diagnostic value ", stringify!($raw), ".")]
182                $variant,
183            )+
184        }
185
186        impl $name {
187            /// Return the fixed numeric fact value.
188            #[must_use]
189            pub const fn raw(self) -> u64 {
190                match self {
191                    $(Self::$variant => $raw,)+
192                }
193            }
194
195            /// Recover a known compact value from its public numeric identity.
196            #[must_use]
197            pub const fn known(raw: u64) -> Option<Self> {
198                match raw {
199                    $($raw => Some(Self::$variant),)+
200                    _ => None,
201                }
202            }
203        }
204
205        impl fmt::Debug for $name {
206            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207                write!(f, "{}", self.raw())
208            }
209        }
210    };
211}
212
213define_numeric_fact_value_registry! {
214    /// Compact accepted constraint family carried by [`DiagnosticFactTag::ConstraintKind`].
215    pub enum DiagnosticConstraintKind {
216        Check = 1;
217        NotNull = 2;
218        Relation = 3;
219        TargetedRule = 4;
220        Unique = 5;
221    }
222}
223
224define_numeric_fact_value_registry! {
225    /// Compact enforcement boundary carried by [`DiagnosticFactTag::ConstraintContext`].
226    pub enum DiagnosticConstraintContext {
227        Integrity = 1;
228        MigrationValidation = 2;
229        WriteAdmission = 3;
230    }
231}
232
233define_numeric_fact_value_registry! {
234    /// Compact storage component carried by [`DiagnosticFactTag::ComponentKind`].
235    pub enum DiagnosticComponentKind {
236        CommitDataKey = 1;
237        IndexKey = 2;
238        IndexKeyComponent = 3;
239        RelationTargetPrimaryKey = 4;
240    }
241}
242
243define_numeric_fact_value_registry! {
244    /// Compact semantic family carried by [`DiagnosticFactTag::TypeFamily`].
245    pub enum DiagnosticTypeFamily {
246        Blob = 1;
247        Bool = 2;
248        Collection = 3;
249        Null = 4;
250        Numeric = 5;
251        Opaque = 6;
252        Structured = 7;
253        Text = 8;
254        Unknown = 9;
255    }
256}
257
258define_numeric_fact_value_registry! {
259    /// Compact function identity carried by [`DiagnosticFactTag::FunctionKind`].
260    pub enum DiagnosticFunctionKind {
261        Abs = 1;
262        Cbrt = 2;
263        Ceiling = 3;
264        Coalesce = 4;
265        CollectionContains = 5;
266        Contains = 6;
267        EndsWith = 7;
268        Exp = 8;
269        Floor = 9;
270        IsEmpty = 10;
271        IsMissing = 11;
272        IsNotEmpty = 12;
273        IsNotNull = 13;
274        IsNull = 14;
275        Left = 15;
276        Length = 16;
277        Ln = 17;
278        Log = 18;
279        Log2 = 19;
280        Log10 = 20;
281        Lower = 21;
282        Ltrim = 22;
283        Mod = 23;
284        NullIf = 24;
285        OctetLength = 25;
286        Position = 26;
287        Power = 27;
288        Replace = 28;
289        Right = 29;
290        Round = 30;
291        Rtrim = 31;
292        Sign = 32;
293        Sqrt = 33;
294        StartsWith = 34;
295        Substring = 35;
296        Trim = 36;
297        Trunc = 37;
298        Upper = 38;
299        InList = 39;
300    }
301}
302
303define_numeric_fact_value_registry! {
304    /// Compact operator identity carried by [`DiagnosticFactTag::OperatorKind`].
305    pub enum DiagnosticOperatorKind {
306        Not = 1;
307        Add = 2;
308        And = 3;
309        Div = 4;
310        Eq = 5;
311        Gt = 6;
312        Gte = 7;
313        Lt = 8;
314        Lte = 9;
315        Mul = 10;
316        Ne = 11;
317        Or = 12;
318        Sub = 13;
319        In = 14;
320        NotIn = 15;
321        Contains = 16;
322        StartsWith = 17;
323        EndsWith = 18;
324    }
325}
326
327define_numeric_fact_value_registry! {
328    /// Compact aggregate identity carried by [`DiagnosticFactTag::AggregateKind`].
329    pub enum DiagnosticAggregateKind {
330        Count = 1;
331        Sum = 2;
332        Avg = 3;
333        Exists = 4;
334        Min = 5;
335        Max = 6;
336        First = 7;
337        Last = 8;
338    }
339}
340
341impl DiagnosticDecodeReason {
342    /// Return the fixed numeric fact value.
343    #[must_use]
344    pub const fn raw(self) -> u64 {
345        match self {
346            Self::CursorEmpty => 1,
347            Self::CursorTooLong => 2,
348            Self::CursorOddLength => 3,
349            Self::CursorInvalidHex => 4,
350            Self::CursorGroupedDirectionMismatch => 5,
351            Self::CursorTokenEncode => 6,
352            Self::CursorTokenDecode => 7,
353            Self::RecoveryMarkerMagic => 8,
354            Self::RecoveryMarkerChecksum => 9,
355            Self::RecoveryMarkerState => 10,
356        }
357    }
358
359    /// Recover a known compact decode reason.
360    #[must_use]
361    pub const fn known(raw: u64) -> Option<Self> {
362        match raw {
363            1 => Some(Self::CursorEmpty),
364            2 => Some(Self::CursorTooLong),
365            3 => Some(Self::CursorOddLength),
366            4 => Some(Self::CursorInvalidHex),
367            5 => Some(Self::CursorGroupedDirectionMismatch),
368            6 => Some(Self::CursorTokenEncode),
369            7 => Some(Self::CursorTokenDecode),
370            8 => Some(Self::RecoveryMarkerMagic),
371            9 => Some(Self::RecoveryMarkerChecksum),
372            10 => Some(Self::RecoveryMarkerState),
373            _ => None,
374        }
375    }
376}
377
378impl DiagnosticMutationOperation {
379    /// Return the fixed numeric fact value.
380    #[must_use]
381    pub const fn raw(self) -> u64 {
382        match self {
383            Self::Insert => 1,
384            Self::Replace => 2,
385            Self::Update => 3,
386            Self::Delete => 4,
387        }
388    }
389
390    /// Recover a known compact mutation operation.
391    #[must_use]
392    pub const fn known(raw: u64) -> Option<Self> {
393        match raw {
394            1 => Some(Self::Insert),
395            2 => Some(Self::Replace),
396            3 => Some(Self::Update),
397            4 => Some(Self::Delete),
398            _ => None,
399        }
400    }
401}
402
403impl fmt::Debug for DiagnosticFactTag {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        write!(f, "{}", self.raw())
406    }
407}
408
409impl fmt::Debug for DiagnosticDecodeReason {
410    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
411        write!(f, "{}", self.raw())
412    }
413}
414
415impl fmt::Debug for DiagnosticMutationOperation {
416    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417        write!(f, "{}", self.raw())
418    }
419}
420
421/// Pack two accepted `u32` identities into one fact value without narrowing.
422#[must_use]
423pub const fn pack_u32_pair(high: u32, low: u32) -> u64 {
424    (high as u64) << 32 | low as u64
425}
426
427/// Recover the two accepted identities from one packed fact value.
428#[must_use]
429pub const fn unpack_u32_pair(value: u64) -> (u32, u32) {
430    let bytes = value.to_be_bytes();
431    (
432        u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
433        u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
434    )
435}
436
437#[cfg(test)]
438mod tests {
439    use super::{
440        DiagnosticAggregateKind, DiagnosticComponentKind, DiagnosticConstraintContext,
441        DiagnosticConstraintKind, DiagnosticDecodeReason, DiagnosticFactTag,
442        DiagnosticFunctionKind, DiagnosticMutationOperation, DiagnosticOperatorKind,
443        DiagnosticTypeFamily, ORDERED_FACT_TAGS, pack_u32_pair, unpack_u32_pair,
444    };
445
446    #[test]
447    fn fact_tag_registry_is_fixed_unique_and_contiguous() {
448        for (index, tag) in ORDERED_FACT_TAGS.iter().copied().enumerate() {
449            let expected = u8::try_from(index + 1).expect("fact-tag index fits u8");
450            assert_eq!(tag.raw(), expected);
451            assert_eq!(DiagnosticFactTag::known(expected), Some(tag));
452        }
453
454        assert_eq!(DiagnosticFactTag::known(0), None);
455        assert_eq!(DiagnosticFactTag::known(90), None);
456        assert_eq!(DiagnosticFactTag::known(u8::MAX), None);
457    }
458
459    #[test]
460    fn accepted_identity_pair_packing_is_exact() {
461        for pair in [
462            (0, 0),
463            (1, 2),
464            (u32::MAX, 0),
465            (0, u32::MAX),
466            (u32::MAX, u32::MAX),
467        ] {
468            assert_eq!(unpack_u32_pair(pack_u32_pair(pair.0, pair.1)), pair);
469        }
470    }
471
472    #[test]
473    fn constraint_fact_value_registries_are_fixed() {
474        assert_eq!(DiagnosticConstraintKind::Check.raw(), 1);
475        assert_eq!(DiagnosticConstraintKind::NotNull.raw(), 2);
476        assert_eq!(DiagnosticConstraintKind::Relation.raw(), 3);
477        assert_eq!(DiagnosticConstraintKind::TargetedRule.raw(), 4);
478        assert_eq!(DiagnosticConstraintKind::Unique.raw(), 5);
479        assert_eq!(DiagnosticConstraintKind::known(0), None);
480        assert_eq!(DiagnosticConstraintKind::known(6), None);
481
482        assert_eq!(DiagnosticConstraintContext::Integrity.raw(), 1);
483        assert_eq!(DiagnosticConstraintContext::MigrationValidation.raw(), 2);
484        assert_eq!(DiagnosticConstraintContext::WriteAdmission.raw(), 3);
485        assert_eq!(DiagnosticConstraintContext::known(0), None);
486        assert_eq!(DiagnosticConstraintContext::known(4), None);
487    }
488
489    #[test]
490    fn component_kind_registry_is_fixed_and_numeric() {
491        let kinds = [
492            DiagnosticComponentKind::CommitDataKey,
493            DiagnosticComponentKind::IndexKey,
494            DiagnosticComponentKind::IndexKeyComponent,
495            DiagnosticComponentKind::RelationTargetPrimaryKey,
496        ];
497
498        for (index, kind) in kinds.iter().copied().enumerate() {
499            let expected = (index + 1) as u64;
500            assert_eq!(kind.raw(), expected);
501            assert_eq!(DiagnosticComponentKind::known(expected), Some(kind));
502            assert_eq!(format!("{kind:?}"), expected.to_string());
503        }
504        assert_eq!(DiagnosticComponentKind::known(0), None);
505        assert_eq!(DiagnosticComponentKind::known(5), None);
506    }
507
508    #[test]
509    fn decode_reason_registry_is_fixed_and_numeric() {
510        let reasons = [
511            DiagnosticDecodeReason::CursorEmpty,
512            DiagnosticDecodeReason::CursorTooLong,
513            DiagnosticDecodeReason::CursorOddLength,
514            DiagnosticDecodeReason::CursorInvalidHex,
515            DiagnosticDecodeReason::CursorGroupedDirectionMismatch,
516            DiagnosticDecodeReason::CursorTokenEncode,
517            DiagnosticDecodeReason::CursorTokenDecode,
518            DiagnosticDecodeReason::RecoveryMarkerMagic,
519            DiagnosticDecodeReason::RecoveryMarkerChecksum,
520            DiagnosticDecodeReason::RecoveryMarkerState,
521        ];
522
523        for (index, reason) in reasons.iter().copied().enumerate() {
524            let expected = (index + 1) as u64;
525            assert_eq!(reason.raw(), expected);
526            assert_eq!(DiagnosticDecodeReason::known(expected), Some(reason));
527            assert_eq!(format!("{reason:?}"), expected.to_string());
528        }
529
530        assert_eq!(DiagnosticDecodeReason::known(0), None);
531        assert_eq!(DiagnosticDecodeReason::known(11), None);
532    }
533
534    #[test]
535    fn mutation_operation_registry_is_fixed_and_numeric() {
536        let operations = [
537            DiagnosticMutationOperation::Insert,
538            DiagnosticMutationOperation::Replace,
539            DiagnosticMutationOperation::Update,
540            DiagnosticMutationOperation::Delete,
541        ];
542
543        for (index, operation) in operations.iter().copied().enumerate() {
544            let expected = (index + 1) as u64;
545            assert_eq!(operation.raw(), expected);
546            assert_eq!(
547                DiagnosticMutationOperation::known(expected),
548                Some(operation)
549            );
550            assert_eq!(format!("{operation:?}"), expected.to_string());
551        }
552
553        assert_eq!(DiagnosticMutationOperation::known(0), None);
554        assert_eq!(DiagnosticMutationOperation::known(5), None);
555    }
556
557    #[test]
558    fn query_kind_registries_are_fixed_contiguous_and_numeric() {
559        for raw in 1..=9 {
560            let value = DiagnosticTypeFamily::known(raw).expect("type family should be known");
561            assert_eq!(value.raw(), raw);
562            assert_eq!(format!("{value:?}"), raw.to_string());
563        }
564        assert_eq!(DiagnosticTypeFamily::known(0), None);
565        assert_eq!(DiagnosticTypeFamily::known(10), None);
566
567        for raw in 1..=39 {
568            let value = DiagnosticFunctionKind::known(raw).expect("function kind should be known");
569            assert_eq!(value.raw(), raw);
570            assert_eq!(format!("{value:?}"), raw.to_string());
571        }
572        assert_eq!(DiagnosticFunctionKind::known(0), None);
573        assert_eq!(DiagnosticFunctionKind::known(40), None);
574
575        for raw in 1..=18 {
576            let value = DiagnosticOperatorKind::known(raw).expect("operator kind should be known");
577            assert_eq!(value.raw(), raw);
578            assert_eq!(format!("{value:?}"), raw.to_string());
579        }
580        assert_eq!(DiagnosticOperatorKind::known(0), None);
581        assert_eq!(DiagnosticOperatorKind::known(19), None);
582
583        for raw in 1..=8 {
584            let value =
585                DiagnosticAggregateKind::known(raw).expect("aggregate kind should be known");
586            assert_eq!(value.raw(), raw);
587            assert_eq!(format!("{value:?}"), raw.to_string());
588        }
589        assert_eq!(DiagnosticAggregateKind::known(0), None);
590        assert_eq!(DiagnosticAggregateKind::known(9), None);
591    }
592}