Skip to main content

sqlx_cloudflare_d1/
types.rs

1//! `Encode`, `Decode` and `Type` for the Rust types D1 can store.
2//!
3//! D1 is SQLite behind a JavaScript API, so the mapping follows sqlx-sqlite
4//! wherever D1 does not force a difference -- code moving from `Sqlite` to
5//! `D1` should behave the same. Where it does force one, it fails loudly:
6//!
7//! | Rust | Stored as | Notes |
8//! |---|---|---|
9//! | `bool` | `INTEGER` 0/1 | any non-zero integer decodes as `true` |
10//! | `i8`, `i16`, `i32`, `u8`, `u16`, `u32` | `INTEGER` | decoding a value that does not fit is an error |
11//! | `i64` | `INTEGER` | only within ±(2^53 − 1), in both directions |
12//! | `f32`, `f64` | `REAL` | also decode from `INTEGER` |
13//! | `&str`, `String`, `Box<str>`, `Arc<str>`, `Cow<str>` | `TEXT` | |
14//! | `&[u8]`, `Vec<u8>`, `Box<[u8]>`, `Arc<[u8]>` | `BLOB` | also decode from `TEXT`, as its bytes |
15//!
16//! `u64` is deliberately missing: there is nothing it could be stored as
17//! without losing most of its range.
18//!
19//! # Integers and JavaScript numbers
20//!
21//! D1 passes values as JavaScript numbers, never `BigInt`, and a number is
22//! exact only up to 2^53 − 1. So binding an `i64` outside ±(2^53 − 1) is an
23//! error, not a value rounded on the way in, and a stored integer beyond that
24//! range comes back as a `REAL` that will not decode as `i64`.
25//!
26//! For the same reason, a whole number stored as `REAL` -- `3.0` -- arrives
27//! indistinguishable from the integer `3`, and is typed `INTEGER`. That is why
28//! the float types decode from `INTEGER` too.
29
30sqlx_cloudflare_core::impl_types!(crate::D1);
31
32// Through `Row::try_get`, as a consumer decodes: that runs `Type::compatible`
33// before `Decode`, and the two have to agree.
34#[cfg(test)]
35mod tests {
36    use std::borrow::Cow;
37    use std::sync::Arc;
38
39    use sqlx_core::arguments::Arguments;
40    use sqlx_core::decode::Decode;
41    use sqlx_core::row::Row;
42    use sqlx_core::types::Type;
43
44    use sqlx_cloudflare_core::Value;
45
46    use crate::{D1ArgumentValue, D1Arguments, D1Row, D1};
47
48    fn get<T>(data: Value) -> Result<T, sqlx_core::error::Error>
49    where
50        T: for<'r> Decode<'r, D1> + Type<D1>,
51    {
52        let rows = D1Row::from_result(vec!["v".into()], vec![vec![data]]).unwrap();
53        rows[0].try_get("v")
54    }
55
56    fn encoded<'t, T: sqlx_core::encode::Encode<'t, D1> + Type<D1>>(value: T) -> D1ArgumentValue {
57        let mut arguments = D1Arguments::default();
58        arguments.add(value).unwrap();
59        arguments.values[0].clone()
60    }
61
62    #[test]
63    fn a_narrow_integer_that_does_not_fit_is_an_error() {
64        assert_eq!(get::<i32>(Value::Integer(5)).unwrap(), 5);
65        assert!(get::<i8>(Value::Integer(300)).is_err());
66        assert!(get::<u8>(Value::Integer(256)).is_err());
67        assert!(get::<u32>(Value::Integer(-1)).is_err());
68        assert_eq!(get::<u32>(Value::Integer(4_294_967_295)).unwrap(), u32::MAX);
69    }
70
71    #[test]
72    fn any_non_zero_integer_is_true() {
73        assert!(!get::<bool>(Value::Integer(0)).unwrap());
74        assert!(get::<bool>(Value::Integer(1)).unwrap());
75        assert!(get::<bool>(Value::Integer(2)).unwrap());
76        assert!(get::<bool>(Value::Integer(-1)).unwrap());
77        assert!(get::<bool>(Value::Text("true".into())).is_err());
78    }
79
80    #[test]
81    fn floats_decode_from_integers_too() {
82        assert!((get::<f32>(Value::Integer(3)).unwrap() - 3.0).abs() < f32::EPSILON);
83        assert!((get::<f64>(Value::Real(0.25)).unwrap() - 0.25).abs() < f64::EPSILON);
84        assert!(get::<f64>(Value::Text("1.5".into())).is_err());
85    }
86
87    #[test]
88    fn bytes_decode_from_text_but_text_not_from_bytes() {
89        assert_eq!(get::<Vec<u8>>(Value::Text("ab".into())).unwrap(), b"ab");
90        assert_eq!(get::<Vec<u8>>(Value::Blob(vec![0, 255])).unwrap(), [0, 255]);
91        assert!(get::<String>(Value::Blob(b"ab".to_vec())).is_err());
92    }
93
94    #[test]
95    fn nothing_converts_between_numbers_and_text() {
96        assert!(get::<String>(Value::Integer(1)).is_err());
97        assert!(get::<i64>(Value::Text("1".into())).is_err());
98        assert!(get::<i64>(Value::Real(1.5)).is_err());
99    }
100
101    #[test]
102    fn null_decodes_only_into_an_option() {
103        assert_eq!(get::<Option<String>>(Value::Null).unwrap(), None);
104        assert!(get::<String>(Value::Null).is_err());
105        assert!(get::<i64>(Value::Null).is_err());
106    }
107
108    #[test]
109    fn every_text_and_byte_container_encodes_the_same() {
110        let text = D1ArgumentValue::Text("x".into());
111        assert_eq!(encoded("x"), text);
112        assert_eq!(encoded(String::from("x")), text);
113        assert_eq!(encoded(Box::<str>::from("x")), text);
114        assert_eq!(encoded(Arc::<str>::from("x")), text);
115        assert_eq!(encoded(Cow::Borrowed("x")), text);
116
117        let blob = D1ArgumentValue::Blob(vec![1, 2]);
118        assert_eq!(encoded(&[1_u8, 2][..]), blob);
119        assert_eq!(encoded(vec![1_u8, 2]), blob);
120        assert_eq!(encoded(Box::<[u8]>::from([1_u8, 2])), blob);
121        assert_eq!(encoded(Arc::<[u8]>::from([1_u8, 2])), blob);
122    }
123
124    #[test]
125    fn narrow_numbers_encode_widened() {
126        assert_eq!(encoded(-7_i8), D1ArgumentValue::Integer(-7));
127        assert_eq!(encoded(u32::MAX), D1ArgumentValue::Integer(4_294_967_295));
128        assert_eq!(encoded(false), D1ArgumentValue::Integer(0));
129        assert_eq!(encoded(0.5_f32), D1ArgumentValue::Real(0.5));
130    }
131}