Skip to main content

rivet/types/
source_column.rs

1//! Native database column metadata captured before mapping to a [`RivetType`].
2//!
3//! See `rivet_roadmap.md` §Epic 14 (type safety). §6 — Internal Type System.
4//! point of this struct is to keep *enough* native-DB metadata to make a
5//! lossless mapping decision later — in particular `precision` / `scale` for
6//! `numeric(p,s)` / `decimal(p,s)` — instead of going straight to
7//! `arrow::DataType` and silently degrading to `Utf8`.
8//!
9//! The struct is intentionally driver-agnostic: PostgreSQL fills it in from
10//! `pg_attribute` / `information_schema`, MySQL from `information_schema`,
11//! and the planner can also synthesize a row from a YAML override (roadmap §8).
12
13use serde::Serialize;
14
15use super::RivetType;
16
17/// Metadata captured from the source database for a single column,
18/// before it is mapped to a [`RivetType`].
19///
20/// Names follow the roadmap struct (§6 SourceColumn) one-for-one so the
21/// document and the code stay in lockstep. Optional fields are populated
22/// only when the source driver actually provides them — Rivet must never
23/// invent a precision/scale to "fill in" a missing one (that is exactly
24/// the silent-degradation pattern the roadmap forbids).
25#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub struct SourceColumn {
27    /// Column name as reported by the source database.
28    pub name: String,
29    /// Native database type identifier — vendor-specific string used for
30    /// reports and policy decisions (e.g. `"numeric"`, `"timestamptz"`,
31    /// `"jsonb"`). Always present.
32    pub native_type: String,
33    /// True when the source schema declares the column nullable.
34    pub nullable: bool,
35    /// Decimal precision — number of total significant digits. Only present
36    /// for fixed-precision numeric types where the source actually declared
37    /// a precision (e.g. `numeric(18,2)` → `Some(18)`; `numeric` → `None`).
38    pub precision: Option<u8>,
39    /// Decimal scale — number of digits to the right of the decimal point.
40    /// Signed because PostgreSQL allows negative scale for `numeric` (e.g.
41    /// `numeric(5,-2)` rounds to hundreds).
42    pub scale: Option<i8>,
43    /// Length for variable-length character/binary types when the source
44    /// declares one (e.g. `varchar(255)` → `Some(255)`).
45    pub length: Option<u64>,
46    /// Fractional-second precision for temporal types when declared
47    /// (e.g. `timestamp(6)` → `Some(6)`).
48    pub datetime_precision: Option<u8>,
49}
50
51impl SourceColumn {
52    /// Convenience constructor for the most common case: a column whose
53    /// native type doesn't carry length / precision metadata.
54    pub fn simple(name: impl Into<String>, native_type: impl Into<String>, nullable: bool) -> Self {
55        Self {
56            name: name.into(),
57            native_type: native_type.into(),
58            nullable,
59            precision: None,
60            scale: None,
61            length: None,
62            datetime_precision: None,
63        }
64    }
65
66    /// Convenience constructor for declared decimal columns.
67    #[allow(dead_code)]
68    pub fn decimal(
69        name: impl Into<String>,
70        native_type: impl Into<String>,
71        nullable: bool,
72        precision: u8,
73        scale: i8,
74    ) -> Self {
75        Self {
76            name: name.into(),
77            native_type: native_type.into(),
78            nullable,
79            precision: Some(precision),
80            scale: Some(scale),
81            length: None,
82            datetime_precision: None,
83        }
84    }
85}
86
87/// A user-supplied YAML override (`exports[].columns:` in `rivet.yaml`,
88/// roadmap §8) carries one of these per column. Stored separately from
89/// [`SourceColumn`] so it's obvious in code review whether a particular
90/// type came from autodetect or from an explicit override.
91///
92/// Wired into the planning pipeline by Chunk 6.
93#[allow(dead_code)]
94#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
95pub struct ColumnOverride {
96    /// Name of the column the override applies to (case-sensitive — matches
97    /// what the source driver returns; mismatch is rejected at config-load).
98    pub name: String,
99    /// The user-declared type. Replaces whatever autodetect produced.
100    pub rivet_type: RivetType,
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn simple_constructor_leaves_optional_metadata_blank() {
109        let c = SourceColumn::simple("id", "int8", false);
110        assert_eq!(c.name, "id");
111        assert_eq!(c.native_type, "int8");
112        assert!(!c.nullable);
113        assert_eq!(c.precision, None);
114        assert_eq!(c.scale, None);
115        assert_eq!(c.length, None);
116        assert_eq!(c.datetime_precision, None);
117    }
118
119    #[test]
120    fn decimal_constructor_sets_precision_scale() {
121        let c = SourceColumn::decimal("amount", "numeric", true, 18, 2);
122        assert_eq!(c.precision, Some(18));
123        assert_eq!(c.scale, Some(2));
124        assert!(c.nullable);
125    }
126
127    #[test]
128    fn negative_scale_is_supported_for_postgres_numeric() {
129        let c = SourceColumn::decimal("rough", "numeric", false, 5, -2);
130        assert_eq!(c.scale, Some(-2));
131    }
132
133    #[test]
134    fn source_column_serializes_nullable_optional_fields() {
135        let c = SourceColumn::decimal("amount", "numeric", true, 18, 2);
136        let json: serde_json::Value =
137            serde_json::from_str(&serde_json::to_string(&c).expect("serialize")).expect("parse");
138        assert_eq!(json["name"], "amount");
139        assert_eq!(json["native_type"], "numeric");
140        assert_eq!(json["nullable"], true);
141        assert_eq!(json["precision"], 18);
142        assert_eq!(json["scale"], 2);
143        assert!(json["length"].is_null());
144        assert!(json["datetime_precision"].is_null());
145    }
146}