Skip to main content

schema_core/common/
generic_value.rs

1use std::collections::BTreeMap;
2use std::hash::{Hash, Hasher};
3
4use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// The canonical value vocabulary every layer trades in — the **middle type**
10/// between a source and a sink.
11///
12/// A source maps its native types *into* these variants; a sink maps them *out*
13/// to its own representation. The set is deliberately fine-grained — numerics are
14/// split by width, temporals are split into date/time/timestamp/timestamptz — so
15/// no semantic information is lost in transit: a `date` arrives at a sink as a
16/// [`Date`](Self::Date), not an opaque string a future sink would have to guess
17/// at. Text-family Postgres types (`text`/`varchar`/`citext`/enum) share the one
18/// [`String`](Self::String) shape because they don't differ *as values*; their
19/// indexing differs, and that lives in the field's `FlussoType`.
20///
21/// Serde is **derived and format-agnostic** on purpose. The derive is externally
22/// tagged (`{"Date":"2024-01-01"}`, `{"BigInt":5}`), so serialize → deserialize
23/// is a lossless identity — a `Date` round-trips back a `Date`, never collapsing
24/// to a string. That lets a queue persist or transport a value in whatever format
25/// it likes and hand it back unchanged: it goes in as a `GenericValue` and comes
26/// out as the same `GenericValue`. Core picks no format (no JSON here); the
27/// concrete encoding is the consumer's choice.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub enum GenericValue {
30    Null,
31    Bool(bool),
32    /// `smallint` / `int2`.
33    SmallInt(i16),
34    /// `integer` / `int4`.
35    Int(i32),
36    /// `bigint` / `int8`.
37    BigInt(i64),
38    /// `real` / `float4`.
39    Float(f32),
40    /// `double precision` / `float8`.
41    Double(f64),
42    /// `numeric` / `decimal` — exact.
43    Decimal(Decimal),
44    /// Any text-family value (`text`/`varchar`/`citext`/enum).
45    String(String),
46    /// `uuid`.
47    Uuid(Uuid),
48    /// `date` — no time, no zone.
49    Date(NaiveDate),
50    /// `time` — no date, no zone.
51    Time(NaiveTime),
52    /// `timestamp` — date + time, no zone.
53    Timestamp(NaiveDateTime),
54    /// `timestamptz` — an instant, normalized to UTC.
55    TimestampTz(DateTime<Utc>),
56    /// `bytea`.
57    Bytes(Vec<u8>),
58    Array(Vec<GenericValue>),
59    Map(BTreeMap<String, GenericValue>),
60}
61
62impl GenericValue {
63    /// Whether this value can stand as a single SQL parameter, key, or literal:
64    /// true for every scalar variant, false for `Null` and the composite
65    /// `Array`/`Map`. The one home for that rule — the Postgres source applies
66    /// it when binding params, building keys, and inlining literals. Written as
67    /// an exhaustive match so a new variant cannot be added without classifying
68    /// it here.
69    pub fn is_bindable_scalar(&self) -> bool {
70        match self {
71            GenericValue::Bool(_)
72            | GenericValue::SmallInt(_)
73            | GenericValue::Int(_)
74            | GenericValue::BigInt(_)
75            | GenericValue::Float(_)
76            | GenericValue::Double(_)
77            | GenericValue::Decimal(_)
78            | GenericValue::String(_)
79            | GenericValue::Uuid(_)
80            | GenericValue::Date(_)
81            | GenericValue::Time(_)
82            | GenericValue::Timestamp(_)
83            | GenericValue::TimestampTz(_)
84            | GenericValue::Bytes(_) => true,
85            GenericValue::Null | GenericValue::Array(_) | GenericValue::Map(_) => false,
86        }
87    }
88}
89
90// `f32`/`f64` are not `Eq`/`Hash`, so the derives won't do; compare and hash the
91// float variants by their bit pattern (a total equivalence — the rare float key
92// is pathological anyway, and this keeps `Eq`/`Hash` consistent). Every other
93// variant defers to its payload's own impls.
94impl PartialEq for GenericValue {
95    fn eq(&self, other: &Self) -> bool {
96        use GenericValue::*;
97        match (self, other) {
98            (Null, Null) => true,
99            (Bool(a), Bool(b)) => a == b,
100            (SmallInt(a), SmallInt(b)) => a == b,
101            (Int(a), Int(b)) => a == b,
102            (BigInt(a), BigInt(b)) => a == b,
103            (Float(a), Float(b)) => a.to_bits() == b.to_bits(),
104            (Double(a), Double(b)) => a.to_bits() == b.to_bits(),
105            (Decimal(a), Decimal(b)) => a == b,
106            (String(a), String(b)) => a == b,
107            (Uuid(a), Uuid(b)) => a == b,
108            (Date(a), Date(b)) => a == b,
109            (Time(a), Time(b)) => a == b,
110            (Timestamp(a), Timestamp(b)) => a == b,
111            (TimestampTz(a), TimestampTz(b)) => a == b,
112            (Bytes(a), Bytes(b)) => a == b,
113            (Array(a), Array(b)) => a == b,
114            (Map(a), Map(b)) => a == b,
115            _ => false,
116        }
117    }
118}
119
120impl Eq for GenericValue {}
121
122impl Hash for GenericValue {
123    fn hash<H: Hasher>(&self, state: &mut H) {
124        use GenericValue::*;
125        std::mem::discriminant(self).hash(state);
126        match self {
127            Null => {}
128            Bool(v) => v.hash(state),
129            SmallInt(v) => v.hash(state),
130            Int(v) => v.hash(state),
131            BigInt(v) => v.hash(state),
132            Float(v) => v.to_bits().hash(state),
133            Double(v) => v.to_bits().hash(state),
134            Decimal(v) => v.hash(state),
135            String(v) => v.hash(state),
136            Uuid(v) => v.hash(state),
137            Date(v) => v.hash(state),
138            Time(v) => v.hash(state),
139            Timestamp(v) => v.hash(state),
140            TimestampTz(v) => v.hash(state),
141            Bytes(v) => v.hash(state),
142            Array(v) => v.hash(state),
143            Map(v) => v.hash(state),
144        }
145    }
146}