Skip to main content

rivet/types/
mod.rs

1//! Rivet's internal type system.
2//!
3//! See `rivet_roadmap.md` §Epic 14 (Warehouse Load Layer). North Star —
4//! *"No silent type degradation"* — is enforced architecturally by
5//! routing every source-column type through the pipeline:
6//!
7//! ```text
8//! Source Native Type
9//!     ↓
10//! SourceColumn  ← what the driver knows about the column
11//!     ↓
12//! RivetType     ← canonical, vendor-independent type
13//!     ↓
14//! TypePolicy    ← strict / lossy / unsupported decisions  (Chunk 4)
15//!     ↓
16//! Arrow DataType + Field metadata  ← physical export type
17//! ```
18//!
19//! This module owns the first three boxes. The fourth (Arrow) is built by
20//! [`mapping::build_arrow_field`]; the fifth (TypePolicy) lands in Chunk 4
21//! of the type-safety milestones (see roadmap §18).
22//!
23//! ## Layer
24//!
25//! Layer-classification (ADR-0003): this module is **planning-layer** — it
26//! only describes / classifies types. It must not perform I/O, log
27//! metrics, or hold any pipeline state. Vendor mappers live in
28//! `crate::source::*` and call into this module.
29
30mod cursor;
31pub mod decimal;
32mod fidelity;
33mod mapping;
34mod override_type;
35pub mod policy;
36mod rivet_type;
37mod source_column;
38pub mod target;
39
40pub use cursor::CursorState;
41pub use fidelity::TypeFidelity;
42pub use mapping::{TypeMapping, build_arrow_field};
43pub use override_type::parse_type_str;
44pub use rivet_type::{RivetType, TimeUnit};
45pub use source_column::SourceColumn;
46// ColumnOverride is the planned public API for column type overrides (Chunk 6).
47#[allow(unused_imports)]
48pub use source_column::ColumnOverride;
49
50/// Per-export column type overrides: column name → declared [`RivetType`].
51///
52/// Built at plan time from the `columns:` map in `rivet.yaml` (roadmap §8).
53/// Passed to [`crate::source::Source::export`] so drivers can use the
54/// declared precision/scale instead of autodetected (often unavailable) metadata.
55pub type ColumnOverrides = std::collections::HashMap<String, RivetType>;
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use crate::types::mapping::{META_FIDELITY, META_LOGICAL_TYPE, META_NATIVE_TYPE};
61    use arrow::datatypes::DataType;
62
63    /// Top-level smoke test: feeding a typical PostgreSQL `payments` table
64    /// through `SourceColumn → RivetType → Arrow Field` produces the schema
65    /// shape demanded by the roadmap's §20 "Definition of Done":
66    ///
67    /// ```text
68    /// id          bigint          int64            Int64                  exact
69    /// amount      numeric(18,2)   decimal(18,2)    Decimal128(18,2)       exact
70    /// created_at  timestamptz     timestamp_tz     Timestamp(us, UTC)     exact
71    /// payload     jsonb           json             Utf8 + metadata        logical_string
72    /// ```
73    #[test]
74    fn end_to_end_payments_schema_matches_definition_of_done() {
75        let cols: Vec<(SourceColumn, RivetType)> = vec![
76            (
77                SourceColumn::simple("id", "bigint", false),
78                RivetType::Int64,
79            ),
80            (
81                SourceColumn::decimal("amount", "numeric", false, 18, 2),
82                RivetType::Decimal {
83                    precision: 18,
84                    scale: 2,
85                },
86            ),
87            (
88                SourceColumn::simple("created_at", "timestamptz", false),
89                RivetType::Timestamp {
90                    unit: TimeUnit::Microsecond,
91                    timezone: Some("UTC".into()),
92                },
93            ),
94            (
95                SourceColumn::simple("payload", "jsonb", true),
96                RivetType::Json,
97            ),
98        ];
99
100        let mappings: Vec<TypeMapping> = cols
101            .into_iter()
102            .map(|(s, t)| TypeMapping::from_source(&s, t))
103            .collect();
104
105        // Fidelity matrix mirrors the table in the Definition of Done.
106        assert_eq!(mappings[0].fidelity, TypeFidelity::Exact);
107        assert_eq!(mappings[1].fidelity, TypeFidelity::Exact);
108        assert_eq!(mappings[2].fidelity, TypeFidelity::Exact);
109        assert_eq!(mappings[3].fidelity, TypeFidelity::LogicalString);
110
111        // Arrow types are exactly what the roadmap demands — no Utf8 fallback for decimal.
112        assert_eq!(mappings[0].arrow_type, Some(DataType::Int64));
113        assert_eq!(mappings[1].arrow_type, Some(DataType::Decimal128(18, 2)));
114        assert!(matches!(
115            mappings[2].arrow_type,
116            Some(DataType::Timestamp(_, Some(_)))
117        ));
118        assert_eq!(mappings[3].arrow_type, Some(DataType::Utf8));
119
120        // Field-level metadata is preserved end-to-end.
121        let amount_field = build_arrow_field(&mappings[1]).expect("amount");
122        assert_eq!(
123            amount_field
124                .metadata()
125                .get(META_NATIVE_TYPE)
126                .map(String::as_str),
127            Some("numeric")
128        );
129        assert_eq!(
130            amount_field
131                .metadata()
132                .get(META_FIDELITY)
133                .map(String::as_str),
134            Some("exact")
135        );
136
137        let payload_field = build_arrow_field(&mappings[3]).expect("payload");
138        assert_eq!(
139            payload_field
140                .metadata()
141                .get(META_LOGICAL_TYPE)
142                .map(String::as_str),
143            Some("json")
144        );
145    }
146}