Skip to main content

rupg_types/
datum.rs

1//! One value of an M1 type and its storage form (spec/07 section 7.9.2).
2//!
3//! The storage form of a fixed-width type is its value in little endian. `bool` is one byte, 0 or 1. `date` is the days since 1 January 2000 as an `i32`, and `timestamp` and `timestamptz` are the microseconds since 1 January 2000 as an `i64`, as in PostgreSQL. `text` is its UTF-8 bytes and `bytea` is its bytes. A NULL has no storage form.
4
5use rupg_common::{Error, Oid, Result};
6
7use crate::typeid::TypeId;
8
9/// One value. `Null` has no type.
10#[derive(Clone, Debug, PartialEq)]
11pub enum Datum {
12    /// SQL NULL.
13    Null,
14    /// `bool`.
15    Bool(bool),
16    /// `int2`.
17    Int2(i16),
18    /// `int4`.
19    Int4(i32),
20    /// `int8`.
21    Int8(i64),
22    /// `float4`.
23    Float4(f32),
24    /// `float8`.
25    Float8(f64),
26    /// `oid`.
27    Oid(Oid),
28    /// `text`.
29    Text(String),
30    /// `bytea`.
31    Bytea(Vec<u8>),
32    /// `uuid`, the 16 bytes in the order of the text form.
33    Uuid([u8; 16]),
34    /// `date`, the days since 1 January 2000. `i32::MAX` is `infinity` and `i32::MIN` is `-infinity`.
35    Date(i32),
36    /// `timestamp`, the microseconds since 1 January 2000. `i64::MAX` is `infinity` and `i64::MIN` is `-infinity`.
37    Timestamp(i64),
38    /// `timestamptz`, the microseconds since 1 January 2000 UTC, with the infinities of `timestamp`.
39    TimestampTz(i64),
40}
41
42impl Datum {
43    /// The type of the value, or `None` for `Null`.
44    pub fn type_id(&self) -> Option<TypeId> {
45        Some(match self {
46            Datum::Null => return None,
47            Datum::Bool(_) => TypeId::BOOL,
48            Datum::Int2(_) => TypeId::INT2,
49            Datum::Int4(_) => TypeId::INT4,
50            Datum::Int8(_) => TypeId::INT8,
51            Datum::Float4(_) => TypeId::FLOAT4,
52            Datum::Float8(_) => TypeId::FLOAT8,
53            Datum::Oid(_) => TypeId::OID,
54            Datum::Text(_) => TypeId::TEXT,
55            Datum::Bytea(_) => TypeId::BYTEA,
56            Datum::Uuid(_) => TypeId::UUID,
57            Datum::Date(_) => TypeId::DATE,
58            Datum::Timestamp(_) => TypeId::TIMESTAMP,
59            Datum::TimestampTz(_) => TypeId::TIMESTAMPTZ,
60        })
61    }
62
63    /// True for `Null`.
64    pub fn is_null(&self) -> bool {
65        matches!(self, Datum::Null)
66    }
67
68    /// The storage form, or `None` for `Null`.
69    pub fn encode(&self) -> Option<Vec<u8>> {
70        Some(match self {
71            Datum::Null => return None,
72            Datum::Bool(v) => vec![u8::from(*v)],
73            Datum::Int2(v) => v.to_le_bytes().to_vec(),
74            Datum::Int4(v) | Datum::Date(v) => v.to_le_bytes().to_vec(),
75            Datum::Int8(v) | Datum::Timestamp(v) | Datum::TimestampTz(v) => {
76                v.to_le_bytes().to_vec()
77            }
78            Datum::Float4(v) => v.to_le_bytes().to_vec(),
79            Datum::Float8(v) => v.to_le_bytes().to_vec(),
80            Datum::Oid(v) => v.0.to_le_bytes().to_vec(),
81            Datum::Text(v) => v.as_bytes().to_vec(),
82            Datum::Bytea(v) => v.clone(),
83            Datum::Uuid(v) => v.to_vec(),
84        })
85    }
86
87    /// The value of type `ty` in the storage form `bytes`. `None` is `Null`. A storage form of the wrong length, a `bool` that is not 0 or 1, or `text` that is not UTF-8 gives SQLSTATE `XX001`, because only the engine writes the storage form. A type that M1 does not store gives `XX000`.
88    pub fn decode(ty: TypeId, bytes: Option<&[u8]>) -> Result<Datum> {
89        let Some(bytes) = bytes else {
90            return Ok(Datum::Null);
91        };
92        let bad = || {
93            Error::corrupted(format!("a stored {ty} value of {} bytes is not valid", bytes.len()))
94        };
95        if ty.width().is_some_and(|w| usize::from(w) != bytes.len()) {
96            return Err(bad());
97        }
98        let n2 = || <[u8; 2]>::try_from(bytes).map_err(|_| bad());
99        let n4 = || <[u8; 4]>::try_from(bytes).map_err(|_| bad());
100        let n8 = || <[u8; 8]>::try_from(bytes).map_err(|_| bad());
101        Ok(match ty {
102            TypeId::BOOL => match bytes {
103                [0] => Datum::Bool(false),
104                [1] => Datum::Bool(true),
105                _ => return Err(bad()),
106            },
107            TypeId::INT2 => Datum::Int2(i16::from_le_bytes(n2()?)),
108            TypeId::INT4 => Datum::Int4(i32::from_le_bytes(n4()?)),
109            TypeId::INT8 => Datum::Int8(i64::from_le_bytes(n8()?)),
110            TypeId::FLOAT4 => Datum::Float4(f32::from_le_bytes(n4()?)),
111            TypeId::FLOAT8 => Datum::Float8(f64::from_le_bytes(n8()?)),
112            TypeId::OID => Datum::Oid(Oid(u32::from_le_bytes(n4()?))),
113            TypeId::DATE => Datum::Date(i32::from_le_bytes(n4()?)),
114            TypeId::TIMESTAMP => Datum::Timestamp(i64::from_le_bytes(n8()?)),
115            TypeId::TIMESTAMPTZ => Datum::TimestampTz(i64::from_le_bytes(n8()?)),
116            TypeId::UUID => Datum::Uuid(bytes.try_into().map_err(|_| bad())?),
117            TypeId::TEXT => Datum::Text(String::from_utf8(bytes.to_vec()).map_err(|_| bad())?),
118            TypeId::BYTEA => Datum::Bytea(bytes.to_vec()),
119            _ => {
120                return Err(Error::internal(format!(
121                    "the type with OID {} has no storage form at M1",
122                    ty.0
123                )));
124            }
125        })
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use rupg_common::SqlState;
132
133    use super::*;
134
135    fn samples() -> Vec<Datum> {
136        vec![
137            Datum::Bool(false),
138            Datum::Bool(true),
139            Datum::Int2(i16::MIN),
140            Datum::Int4(-7),
141            Datum::Int8(i64::MAX),
142            Datum::Float4(-0.5),
143            Datum::Float8(f64::INFINITY),
144            Datum::Oid(Oid(16384)),
145            Datum::Text("héllo".into()),
146            Datum::Text(String::new()),
147            Datum::Bytea(vec![0, 255, 7]),
148            Datum::Uuid([9; 16]),
149            Datum::Date(i32::MIN),
150            Datum::Timestamp(-1),
151            Datum::TimestampTz(i64::MAX),
152        ]
153    }
154
155    #[test]
156    fn storage_forms() {
157        for d in samples() {
158            let ty = d.type_id().unwrap();
159            let bytes = d.encode().unwrap();
160            if let Some(w) = ty.width() {
161                assert_eq!(bytes.len(), usize::from(w), "{ty}");
162            }
163            assert_eq!(Datum::decode(ty, Some(&bytes)).unwrap(), d);
164            assert_eq!(TypeId::from_oid(ty.0), Some(ty));
165            assert_ne!(ty.name(), "unknown");
166            assert_ne!(ty.sql_name(), "unknown");
167        }
168        assert_eq!(Datum::Int4(0x0102_0304).encode().unwrap(), [4, 3, 2, 1]);
169        assert_eq!((Datum::Null.encode(), Datum::Null.type_id()), (None, None));
170        assert!(Datum::decode(TypeId::TEXT, None).unwrap().is_null());
171        let nan = Datum::decode(TypeId::FLOAT8, Some(&f64::NAN.to_le_bytes())).unwrap();
172        assert!(matches!(nan, Datum::Float8(v) if v.is_nan()));
173        assert_eq!(TypeId::from_oid(Oid(1700)), None);
174        assert_eq!((TypeId::INT4.sql_name(), TypeId::TEXT.sql_name()), ("integer", "text"));
175    }
176
177    #[test]
178    fn bad_storage_forms() {
179        let bad: [(TypeId, &[u8]); 5] = [
180            (TypeId::INT4, &[1, 2, 3]),
181            (TypeId::BOOL, &[2]),
182            (TypeId::UUID, &[0; 15]),
183            (TypeId::TEXT, &[0xff, 0xfe]),
184            (TypeId::INT8, &[]),
185        ];
186        for (ty, bytes) in bad {
187            assert_eq!(
188                Datum::decode(ty, Some(bytes)).unwrap_err().state(),
189                SqlState::DATA_CORRUPTED,
190                "{ty}"
191            );
192        }
193        let numeric = TypeId(Oid(1700));
194        assert_eq!(
195            Datum::decode(numeric, Some(&[0])).unwrap_err().state(),
196            SqlState::INTERNAL_ERROR
197        );
198        assert_eq!(numeric.name(), "unknown");
199    }
200}