Skip to main content

ocre/
sql.rs

1//! Query parameters and SQLite value conversions. Pure Rust: no JavaScript
2//! values are created until a statement is bound (see `runtime::d1`).
3
4/// A value bound to a `?N` placeholder of a D1 query.
5///
6/// Build a list with [`params!`](crate::params), which calls
7/// [`IntoParam::into_param`] on each value, in placeholder order: the first
8/// value binds `?1`. A `Param` is a SQLite `NULL`, number or text; no
9/// JavaScript value exists until the statement is bound, so building one is
10/// pure Rust and compares with `==`.
11///
12/// # Examples
13///
14/// ```
15/// use ocre::{IntoParam, params};
16///
17/// let title = String::from("Hello");
18/// let params = params![&title, 42, true, None::<i64>];
19/// assert_eq!(params.len(), 4);
20/// assert_eq!(params[0], "Hello".into_param());
21/// assert_eq!(params[2], 1_i32.into_param()); // booleans are INTEGER 1/0
22/// ```
23#[derive(Debug, Clone, PartialEq)]
24pub struct Param(pub(crate) Value);
25
26#[derive(Debug, Clone, PartialEq)]
27pub(crate) enum Value {
28    Null,
29    Number(f64),
30    Text(String),
31}
32
33/// One SQL statement with its parameters, for [`Db::batch`](crate::Db::batch).
34///
35/// # Examples
36///
37/// ```
38/// use ocre::{Statement, params};
39///
40/// let stmt = Statement::new("DELETE FROM posts WHERE id = ?1", params![7]);
41/// assert_eq!(stmt.sql, "DELETE FROM posts WHERE id = ?1");
42/// assert_eq!(stmt.params, params![7]);
43/// ```
44#[derive(Debug, Clone, PartialEq)]
45pub struct Statement {
46    /// SQL text with `?1, ?2...` placeholders.
47    pub sql: String,
48    /// Values for the placeholders, in order (build with [`params!`](crate::params)).
49    pub params: Vec<Param>,
50}
51
52impl Statement {
53    /// Pairs `sql` with its `params`.
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use ocre::{Statement, params};
59    ///
60    /// let stmt = Statement::new("UPDATE posts SET title = ?1 WHERE id = ?2", params!["New", 3]);
61    /// assert_eq!(stmt.params.len(), 2);
62    /// ```
63    pub fn new(sql: impl Into<String>, params: Vec<Param>) -> Self {
64        Self { sql: sql.into(), params }
65    }
66}
67
68/// Types that can be bound as D1 query parameters.
69///
70/// Implemented for text (`&str`, `String`, `&String`), `bool` (INTEGER 1/0),
71/// `f64`, integers up to 32 bits, `i64` (see [`MAX_SAFE_INTEGER`]),
72/// [`serde_json::Value`] (stored as compact JSON text), `Option<T>` (`None` is
73/// `NULL`) and [`Param`] itself. [`params!`](crate::params) calls it on each
74/// value; implement it for your own types (e.g. an enum stored as text) by
75/// delegating to one of these.
76///
77/// # Examples
78///
79/// ```
80/// use ocre::{IntoParam, Param, params};
81///
82/// enum Status {
83///     Draft,
84///     Published,
85/// }
86///
87/// impl IntoParam for Status {
88///     fn into_param(self) -> Param {
89///         match self {
90///             Status::Draft => "draft",
91///             Status::Published => "published",
92///         }
93///         .into_param()
94///     }
95/// }
96///
97/// assert_eq!(params![Status::Published], params!["published"]);
98/// # let _ = Status::Draft;
99/// ```
100pub trait IntoParam {
101    /// Converts the value into a bound parameter.
102    fn into_param(self) -> Param;
103}
104
105impl IntoParam for Param {
106    fn into_param(self) -> Param {
107        self
108    }
109}
110
111impl IntoParam for &str {
112    fn into_param(self) -> Param {
113        Param(Value::Text(self.to_owned()))
114    }
115}
116
117impl IntoParam for String {
118    fn into_param(self) -> Param {
119        Param(Value::Text(self))
120    }
121}
122
123impl IntoParam for &String {
124    fn into_param(self) -> Param {
125        Param(Value::Text(self.clone()))
126    }
127}
128
129/// SQLite has no boolean type: `true`/`false` are stored as INTEGER 1/0.
130/// Read them back with `#[serde(deserialize_with = "ocre::bool_from_sql")]`.
131impl IntoParam for bool {
132    fn into_param(self) -> Param {
133        Param(Value::Number(if self { 1.0 } else { 0.0 }))
134    }
135}
136
137impl IntoParam for f64 {
138    fn into_param(self) -> Param {
139        Param(Value::Number(self))
140    }
141}
142
143macro_rules! lossless_number_param {
144    ($($ty:ty),*) => {$(
145        impl IntoParam for $ty {
146            fn into_param(self) -> Param {
147                Param(Value::Number(f64::from(self)))
148            }
149        }
150    )*};
151}
152
153lossless_number_param!(i8, i16, i32, u8, u16, u32);
154
155/// Largest integer a JavaScript number (and so D1) represents exactly: 2^53 - 1.
156///
157/// D1 returns INTEGER columns as JavaScript numbers, so rows holding larger
158/// values cannot be read back into `i64`: validate input against this bound
159/// with [`Validator::safe_integer`](crate::Validator::safe_integer). Binding a
160/// larger `i64` sends it as decimal text instead of a rounded number.
161///
162/// # Examples
163///
164/// ```
165/// use ocre::{IntoParam, MAX_SAFE_INTEGER};
166///
167/// assert_eq!(MAX_SAFE_INTEGER, 9_007_199_254_740_991);
168/// let too_big = MAX_SAFE_INTEGER + 1;
169/// assert_eq!(too_big.into_param(), too_big.to_string().into_param());
170/// ```
171pub const MAX_SAFE_INTEGER: i64 = (1 << 53) - 1;
172
173/// Integers within ±(2^53 - 1) are bound as numbers. Larger values are bound
174/// as decimal text, exact for comparisons in `WHERE` clauses.
175impl IntoParam for i64 {
176    fn into_param(self) -> Param {
177        if (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&self) {
178            Param(Value::Number(self as f64))
179        } else {
180            Param(Value::Text(self.to_string()))
181        }
182    }
183}
184
185impl<T: IntoParam> IntoParam for Option<T> {
186    fn into_param(self) -> Param {
187        self.map_or(Param(Value::Null), IntoParam::into_param)
188    }
189}
190
191/// JSON values are stored as their compact text in a `TEXT` column (the
192/// generators add `CHECK (json_valid(<column>))`). Read them back with
193/// `#[serde(deserialize_with = "ocre::json_from_sql")]`.
194impl IntoParam for serde_json::Value {
195    fn into_param(self) -> Param {
196        Param(Value::Text(self.to_string()))
197    }
198}
199
200/// Deserializes a SQLite boolean column (INTEGER 0/1) into `bool`.
201///
202/// Use it as `#[serde(deserialize_with = "ocre::bool_from_sql")]`. Any
203/// non-zero number is `true`; real booleans are accepted too, so the same
204/// struct also reads JSON bodies and cached rows.
205///
206/// # Errors
207///
208/// Fails with the deserializer's error for anything but a boolean or a number
209/// ("expected a boolean or the integer 0 or 1"); [`Db`](crate::Db) queries
210/// report it as [`Error::Internal`](crate::Error::Internal).
211///
212/// # Examples
213///
214/// ```
215/// use serde::Deserialize;
216///
217/// #[derive(Deserialize)]
218/// struct Post {
219///     #[serde(deserialize_with = "ocre::bool_from_sql")]
220///     published: bool,
221/// }
222///
223/// let post: Post = serde_json::from_str(r#"{"published": 1}"#).unwrap();
224/// assert!(post.published);
225/// let post: Post = serde_json::from_str(r#"{"published": false}"#).unwrap();
226/// assert!(!post.published);
227/// assert!(serde_json::from_str::<Post>(r#"{"published": "yes"}"#).is_err());
228/// ```
229pub fn bool_from_sql<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
230    struct Visitor;
231
232    impl serde::de::Visitor<'_> for Visitor {
233        type Value = bool;
234
235        fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236            f.write_str("a boolean or the integer 0 or 1")
237        }
238
239        fn visit_bool<E: serde::de::Error>(self, v: bool) -> Result<bool, E> {
240            Ok(v)
241        }
242
243        fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<bool, E> {
244            Ok(v != 0)
245        }
246
247        fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<bool, E> {
248            Ok(v != 0)
249        }
250
251        fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<bool, E> {
252            Ok(v != 0.0)
253        }
254    }
255
256    deserializer.deserialize_any(Visitor)
257}
258
259/// Deserializes a JSON column (the JSON text D1 returns) into [`serde_json::Value`].
260///
261/// Use it as `#[serde(deserialize_with = "ocre::json_from_sql")]` on a
262/// `serde_json::Value` field. A string is parsed as JSON text; a value that is
263/// not a string (e.g. a row cached as JSON) is taken as is.
264///
265/// # Errors
266///
267/// Fails with the deserializer's error when the text is not valid JSON;
268/// [`Db`](crate::Db) queries report it as [`Error::Internal`](crate::Error::Internal).
269///
270/// # Examples
271///
272/// ```
273/// use serde::Deserialize;
274///
275/// #[derive(Deserialize)]
276/// struct Post {
277///     #[serde(deserialize_with = "ocre::json_from_sql")]
278///     metadata: serde_json::Value,
279/// }
280///
281/// // As D1 returns it: JSON text in a TEXT column.
282/// let post: Post = serde_json::from_str(r#"{"metadata": "{\"tags\":[\"rust\"]}"}"#).unwrap();
283/// assert_eq!(post.metadata["tags"][0], "rust");
284/// // Already parsed (a cached row): kept as is.
285/// let post: Post = serde_json::from_str(r#"{"metadata": {"tags": []}}"#).unwrap();
286/// assert_eq!(post.metadata, serde_json::json!({"tags": []}));
287/// ```
288pub fn json_from_sql<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<serde_json::Value, D::Error> {
289    parse_json_text(serde::Deserialize::deserialize(deserializer)?)
290}
291
292/// Like [`json_from_sql`] for a `NULL`-able JSON column: `NULL` is `None`.
293///
294/// Use it as `#[serde(default, deserialize_with = "ocre::optional_json_from_sql")]`
295/// on an `Option<serde_json::Value>` field (`default` also accepts a missing key).
296///
297/// # Errors
298///
299/// Fails with the deserializer's error when the text is not valid JSON;
300/// [`Db`](crate::Db) queries report it as [`Error::Internal`](crate::Error::Internal).
301///
302/// # Examples
303///
304/// ```
305/// use serde::Deserialize;
306///
307/// #[derive(Deserialize)]
308/// struct Post {
309///     #[serde(default, deserialize_with = "ocre::optional_json_from_sql")]
310///     metadata: Option<serde_json::Value>,
311/// }
312///
313/// let post: Post = serde_json::from_str(r#"{"metadata": null}"#).unwrap();
314/// assert_eq!(post.metadata, None);
315/// let post: Post = serde_json::from_str(r#"{"metadata": "[1, 2]"}"#).unwrap();
316/// assert_eq!(post.metadata, Some(serde_json::json!([1, 2])));
317/// ```
318pub fn optional_json_from_sql<'de, D: serde::Deserializer<'de>>(
319    deserializer: D,
320) -> Result<Option<serde_json::Value>, D::Error> {
321    <Option<serde_json::Value> as serde::Deserialize>::deserialize(deserializer)?.map(parse_json_text).transpose()
322}
323
324fn parse_json_text<E: serde::de::Error>(value: serde_json::Value) -> Result<serde_json::Value, E> {
325    match value {
326        serde_json::Value::String(text) => serde_json::from_str(&text).map_err(E::custom),
327        other => Ok(other),
328    }
329}
330
331#[cfg(test)]
332#[path = "../tests/sql.rs"]
333mod tests;