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}