Skip to main content

sqlx_cloudflare_do/
types.rs

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