Skip to main content

rustlavel_db/
value.rs

1//! The values that cross the database boundary.
2//!
3//! One enum for everything a column can hold and everything a query can bind,
4//! plus conversions so application code deals in Rust types rather than in
5//! `Value` — `row.get::<i64>("id")`, not a match on a variant.
6
7use rustlavel_core::{Error, Json, Result};
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq)]
11pub enum Value {
12    Null,
13    Bool(bool),
14    Int(i64),
15    Float(f64),
16    Text(String),
17    Bytes(Vec<u8>),
18    /// A `json`/`jsonb` column, kept parsed.
19    Json(Json),
20}
21
22impl Value {
23    pub fn is_null(&self) -> bool {
24        matches!(self, Value::Null)
25    }
26
27    /// How this value is written into SQL text for the wire.
28    ///
29    /// Parameters are sent out of band (never interpolated into the statement),
30    /// so this is only the textual encoding of a bound parameter.
31    pub fn to_sql_text(&self) -> Option<String> {
32        match self {
33            Value::Null => None,
34            Value::Bool(true) => Some("t".into()),
35            Value::Bool(false) => Some("f".into()),
36            Value::Int(n) => Some(n.to_string()),
37            Value::Float(n) => Some(n.to_string()),
38            Value::Text(s) => Some(s.clone()),
39            Value::Json(j) => Some(j.to_string()),
40            // A bytea parameter goes over in the hex format Postgres expects.
41            Value::Bytes(bytes) => {
42                let mut out = String::with_capacity(2 + bytes.len() * 2);
43                out.push_str("\\x");
44                for byte in bytes {
45                    out.push_str(&format!("{byte:02x}"));
46                }
47                Some(out)
48            }
49        }
50    }
51
52    /// Render for a human: log lines, Telescope, error messages.
53    pub fn to_display(&self) -> String {
54        match self {
55            Value::Null => "NULL".into(),
56            Value::Bytes(bytes) => format!("<{} bytes>", bytes.len()),
57            other => other.to_sql_text().unwrap_or_default(),
58        }
59    }
60}
61
62impl fmt::Display for Value {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        f.write_str(&self.to_display())
65    }
66}
67
68impl From<Value> for Json {
69    fn from(value: Value) -> Json {
70        match value {
71            Value::Null => Json::Null,
72            Value::Bool(b) => Json::Bool(b),
73            Value::Int(n) => Json::Number(n as f64),
74            Value::Float(n) => Json::Number(n),
75            Value::Text(s) => Json::String(s),
76            Value::Json(j) => j,
77            // Binary has no JSON representation; the length is the useful part.
78            Value::Bytes(bytes) => Json::String(format!("<{} bytes>", bytes.len())),
79        }
80    }
81}
82
83macro_rules! from_int {
84    ($($t:ty),*) => {
85        $(impl From<$t> for Value {
86            fn from(v: $t) -> Value {
87                Value::Int(v as i64)
88            }
89        })*
90    };
91}
92from_int!(i8, i16, i32, i64, u8, u16, u32, usize, isize);
93
94impl From<bool> for Value {
95    fn from(v: bool) -> Value {
96        Value::Bool(v)
97    }
98}
99
100impl From<f32> for Value {
101    fn from(v: f32) -> Value {
102        Value::Float(v as f64)
103    }
104}
105
106impl From<f64> for Value {
107    fn from(v: f64) -> Value {
108        Value::Float(v)
109    }
110}
111
112impl From<String> for Value {
113    fn from(v: String) -> Value {
114        Value::Text(v)
115    }
116}
117
118impl From<&str> for Value {
119    fn from(v: &str) -> Value {
120        Value::Text(v.to_string())
121    }
122}
123
124impl From<&String> for Value {
125    fn from(v: &String) -> Value {
126        Value::Text(v.clone())
127    }
128}
129
130impl From<Vec<u8>> for Value {
131    fn from(v: Vec<u8>) -> Value {
132        Value::Bytes(v)
133    }
134}
135
136impl From<Json> for Value {
137    fn from(v: Json) -> Value {
138        Value::Json(v)
139    }
140}
141
142impl<T: Into<Value>> From<Option<T>> for Value {
143    fn from(v: Option<T>) -> Value {
144        v.map_or(Value::Null, Into::into)
145    }
146}
147
148/// A Rust type a column can be read into.
149///
150/// The conversions are deliberately forgiving in one direction only: an `Int`
151/// column reads into `f64`, but text never silently parses into a number, so a
152/// schema mistake surfaces as an error instead of a wrong value.
153pub trait FromValue: Sized {
154    fn from_value(value: &Value) -> Result<Self>;
155}
156
157fn mismatch<T>(value: &Value) -> Result<T> {
158    Err(Error::msg(format!(
159        "cannot read a {} column as {}",
160        variant_name(value),
161        std::any::type_name::<T>()
162    )))
163}
164
165fn variant_name(value: &Value) -> &'static str {
166    match value {
167        Value::Null => "NULL",
168        Value::Bool(_) => "boolean",
169        Value::Int(_) => "integer",
170        Value::Float(_) => "float",
171        Value::Text(_) => "text",
172        Value::Bytes(_) => "bytea",
173        Value::Json(_) => "json",
174    }
175}
176
177impl FromValue for Value {
178    fn from_value(value: &Value) -> Result<Self> {
179        Ok(value.clone())
180    }
181}
182
183impl FromValue for String {
184    fn from_value(value: &Value) -> Result<Self> {
185        match value {
186            Value::Text(s) => Ok(s.clone()),
187            Value::Json(j) => Ok(j.to_string()),
188            other => mismatch(other),
189        }
190    }
191}
192
193impl FromValue for i64 {
194    fn from_value(value: &Value) -> Result<Self> {
195        match value {
196            Value::Int(n) => Ok(*n),
197            other => mismatch(other),
198        }
199    }
200}
201
202impl FromValue for i32 {
203    fn from_value(value: &Value) -> Result<Self> {
204        i64::from_value(value).map(|n| n as i32)
205    }
206}
207
208impl FromValue for f64 {
209    fn from_value(value: &Value) -> Result<Self> {
210        match value {
211            Value::Float(n) => Ok(*n),
212            Value::Int(n) => Ok(*n as f64),
213            other => mismatch(other),
214        }
215    }
216}
217
218impl FromValue for bool {
219    fn from_value(value: &Value) -> Result<Self> {
220        match value {
221            Value::Bool(b) => Ok(*b),
222            // **MySQL has no boolean.** `BOOLEAN` is an alias for
223            // `tinyint(1)`, and nothing on the wire tells a flag from a small
224            // counter — so the driver cannot know, and hands back an integer.
225            // The model does know: it declared the field `bool`. This is the
226            // layer where that information exists, and resolving it here is
227            // what keeps a `#[derive(Model)]` with a flag on it readable on
228            // all three databases rather than on one.
229            Value::Int(0) => Ok(false),
230            Value::Int(1) => Ok(true),
231            // Anything else is a column being read as the wrong thing — a
232            // counter declared `bool`, most likely — and guessing `true` for
233            // it would hide that for as long as the value stayed non-zero.
234            Value::Int(n) => Err(Error::msg(format!(
235                "cannot read {n} as a bool. A `bool` field maps to 0 or 1; this column holds \
236                 something else, so it is probably a number rather than a flag."
237            ))),
238            other => mismatch(other),
239        }
240    }
241}
242
243impl FromValue for Vec<u8> {
244    fn from_value(value: &Value) -> Result<Self> {
245        match value {
246            Value::Bytes(bytes) => Ok(bytes.clone()),
247            Value::Text(s) => Ok(s.clone().into_bytes()),
248            other => mismatch(other),
249        }
250    }
251}
252
253impl FromValue for Json {
254    fn from_value(value: &Value) -> Result<Self> {
255        match value {
256            Value::Json(j) => Ok(j.clone()),
257            Value::Text(s) => Json::parse(s),
258            other => Ok(other.clone().into()),
259        }
260    }
261}
262
263/// A nullable column reads into `Option<T>`; every other type treats NULL as an
264/// error, which is what makes a missing value impossible to ignore by accident.
265impl<T: FromValue> FromValue for Option<T> {
266    fn from_value(value: &Value) -> Result<Self> {
267        match value {
268            Value::Null => Ok(None),
269            other => T::from_value(other).map(Some),
270        }
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn converts_from_rust_types() {
280        assert_eq!(Value::from(7i32), Value::Int(7));
281        assert_eq!(Value::from("hi"), Value::Text("hi".into()));
282        assert_eq!(Value::from(true), Value::Bool(true));
283        assert_eq!(Value::from(Option::<i64>::None), Value::Null);
284        assert_eq!(Value::from(Some(3i64)), Value::Int(3));
285    }
286
287    #[test]
288    fn reads_into_rust_types() {
289        assert_eq!(i64::from_value(&Value::Int(7)).unwrap(), 7);
290        assert_eq!(f64::from_value(&Value::Int(7)).unwrap(), 7.0);
291        assert_eq!(String::from_value(&Value::Text("a".into())).unwrap(), "a");
292        assert_eq!(Option::<i64>::from_value(&Value::Null).unwrap(), None);
293    }
294
295    #[test]
296    fn a_null_column_read_as_a_non_option_is_an_error() {
297        let error = i64::from_value(&Value::Null).unwrap_err();
298        assert!(error.to_string().contains("NULL"));
299    }
300
301    /// MySQL has no boolean: `BOOLEAN` is `tinyint(1)`, and the driver hands
302    /// back an integer because nothing on the wire tells a flag from a small
303    /// counter. A model that declared the field `bool` does know, so this is
304    /// where the two are reconciled — without it, every model with a flag on
305    /// it is readable on PostgreSQL and SQL Server and not on MySQL.
306    #[test]
307    fn a_mysql_tinyint_reads_into_a_bool_but_a_counter_does_not() {
308        assert!(!bool::from_value(&Value::Int(0)).unwrap());
309        assert!(bool::from_value(&Value::Int(1)).unwrap());
310        assert!(bool::from_value(&Value::Bool(true)).unwrap());
311
312        // Not "any non-zero is true": a counter column declared `bool` is a
313        // mistake, and guessing would hide it for as long as it stayed
314        // non-zero.
315        let error = bool::from_value(&Value::Int(7)).unwrap_err().to_string();
316        assert!(error.contains("cannot read 7 as a bool"), "{error}");
317        assert!(error.contains("probably a number rather than a flag"), "{error}");
318
319        // And text still never becomes a flag.
320        assert!(bool::from_value(&Value::Text("true".into())).is_err());
321    }
322
323    #[test]
324    fn text_never_silently_becomes_a_number() {
325        assert!(i64::from_value(&Value::Text("7".into())).is_err());
326    }
327
328    #[test]
329    fn encodes_parameters_as_postgres_text() {
330        assert_eq!(Value::Bool(true).to_sql_text().as_deref(), Some("t"));
331        assert_eq!(Value::Null.to_sql_text(), None);
332        assert_eq!(Value::Bytes(vec![0xde, 0xad]).to_sql_text().as_deref(), Some("\\xdead"));
333    }
334}