Skip to main content

rivet/types/
fidelity.rs

1//! Type fidelity classification.
2//!
3//! See `rivet_roadmap.md` §Epic 14. Type Fidelity Levels: Every
4//! source-column → Arrow mapping is tagged with one of these levels so the
5//! type policy and the type-report CLI can reason about what the user is
6//! actually getting on disk.
7//!
8//! - [`TypeFidelity::Exact`]      — value and type semantics fully preserved
9//!   (e.g. `numeric(18,2)` → `Decimal128(18,2)` → Parquet `DECIMAL(18,2)`).
10//! - [`TypeFidelity::Compatible`] — value preserved, the on-disk physical
11//!   type differs but a logical-type marker keeps the original semantics
12//!   recoverable (e.g. `uuid` → `Utf8 + metadata logical=uuid`).
13//! - [`TypeFidelity::LogicalString`] — value preserved as text, native
14//!   semantics are *not* guaranteed (e.g. `jsonb` → `Utf8 + metadata
15//!   logical=json`).
16//! - [`TypeFidelity::Lossy`]      — precision or semantics may be lost
17//!   (e.g. `decimal(18,2)` → `Float64`). Forbidden in strict mode.
18//! - [`TypeFidelity::Unsupported`] — Rivet refuses to export the column
19//!   without an explicit policy override.
20
21use serde::Serialize;
22
23/// Fidelity tag attached to every [`crate::types::TypeMapping`].
24///
25/// The variants are deliberately ordered from "best" to "worst" so that
26/// downstream code (CLI report, strict-mode gate) can compare them with
27/// `PartialOrd` semantics:
28/// `Exact > Compatible > LogicalString > Lossy > Unsupported`.
29// `Lossy` and `is_unsafe_for_strict_mode` are used by TypePolicy (Chunk 4).
30#[allow(dead_code)]
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
32#[serde(rename_all = "snake_case")]
33pub enum TypeFidelity {
34    /// Value and type semantics fully preserved on disk.
35    Exact,
36    /// Value preserved; physical type differs, logical-type metadata
37    /// recovers the original semantics.
38    Compatible,
39    /// Value preserved as text; native type semantics are not guaranteed.
40    LogicalString,
41    /// Precision or semantics may be lost. Strict mode rejects this.
42    Lossy,
43    /// Rivet does not safely support this type. Strict mode rejects this.
44    Unsupported,
45}
46
47impl TypeFidelity {
48    /// Stable lowercase string label for persistence, JSON output, and
49    /// human-readable reports. Prefer this over `format!("{:?}")` —
50    /// `Debug` output is not a stable contract.
51    pub fn label(self) -> &'static str {
52        match self {
53            TypeFidelity::Exact => "exact",
54            TypeFidelity::Compatible => "compatible",
55            TypeFidelity::LogicalString => "logical_string",
56            TypeFidelity::Lossy => "lossy",
57            TypeFidelity::Unsupported => "unsupported",
58        }
59    }
60
61    /// True when the fidelity level is one that strict mode must reject
62    /// without an explicit policy override (roadmap §7 "Strict mode
63    /// behavior"). Used by TypePolicy (Chunk 4).
64    #[allow(dead_code)]
65    pub fn is_unsafe_for_strict_mode(self) -> bool {
66        matches!(self, TypeFidelity::Lossy | TypeFidelity::Unsupported)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn fidelity_label_round_trips_through_json() {
76        let cases = [
77            (TypeFidelity::Exact, "\"exact\""),
78            (TypeFidelity::Compatible, "\"compatible\""),
79            (TypeFidelity::LogicalString, "\"logical_string\""),
80            (TypeFidelity::Lossy, "\"lossy\""),
81            (TypeFidelity::Unsupported, "\"unsupported\""),
82        ];
83        for (f, expected_json) in cases {
84            assert_eq!(
85                serde_json::to_string(&f).expect("serialize fidelity"),
86                expected_json,
87                "JSON shape for {:?} must match label() so CLI --json output is stable",
88                f
89            );
90            assert_eq!(f.label(), expected_json.trim_matches('"'));
91        }
92    }
93
94    #[test]
95    fn ordering_is_best_to_worst() {
96        assert!(TypeFidelity::Exact < TypeFidelity::Compatible);
97        assert!(TypeFidelity::Compatible < TypeFidelity::LogicalString);
98        assert!(TypeFidelity::LogicalString < TypeFidelity::Lossy);
99        assert!(TypeFidelity::Lossy < TypeFidelity::Unsupported);
100    }
101
102    #[test]
103    fn strict_mode_only_rejects_lossy_and_unsupported() {
104        assert!(!TypeFidelity::Exact.is_unsafe_for_strict_mode());
105        assert!(!TypeFidelity::Compatible.is_unsafe_for_strict_mode());
106        assert!(!TypeFidelity::LogicalString.is_unsafe_for_strict_mode());
107        assert!(TypeFidelity::Lossy.is_unsafe_for_strict_mode());
108        assert!(TypeFidelity::Unsupported.is_unsafe_for_strict_mode());
109    }
110}