Skip to main content

spin_sdk/
mysql.rs

1//! MySQL relational database storage.
2//!
3//! You can use the [`Decode`] trait to convert a [`DbValue`] to a
4//! suitable Rust type. The following table shows available conversions.
5//!
6//! # Types
7//!
8//! | Rust type | WIT (db-value)      | MySQL type(s)           |
9//! |-----------|---------------------|-------------------------|
10//! | `bool`    | int8(s8)            | TINYINT(1), BOOLEAN     |
11//! | `i8`      | int8(s8)            | TINYINT                 |
12//! | `i16`     | int16(s16)          | SMALLINT                |
13//! | `i32`     | int32(s32)          | MEDIUM, INT             |
14//! | `i64`     | int64(s64)          | BIGINT                  |
15//! | `u8`      | uint8(u8)           | TINYINT UNSIGNED        |
16//! | `u16`     | uint16(u16)         | SMALLINT UNSIGNED       |
17//! | `u32`     | uint32(u32)         | INT UNSIGNED            |
18//! | `u64`     | uint64(u64)         | BIGINT UNSIGNED         |
19//! | `f32`     | floating32(float32) | FLOAT                   |
20//! | `f64`     | floating64(float64) | DOUBLE                  |
21//! | `String`  | str(string)         | VARCHAR, CHAR, TEXT     |
22//! | `Vec<u8>` | binary(list\<u8\>)  | VARBINARY, BINARY, BLOB |
23
24#[doc(hidden)]
25/// Module containing wit bindgen generated code.
26///
27/// This is only meant for internal consumption.
28pub mod wit {
29    #![allow(missing_docs)]
30    use crate::wit_bindgen;
31
32    wit_bindgen::generate!({
33        runtime_path: "crate::wit_bindgen::rt",
34        world: "spin-sdk-mysql",
35        path: "wit",
36        generate_all,
37    });
38}
39
40/// An open connection to a MySQL database.
41///
42/// # Examples
43///
44/// Load a set of rows from a local MySQL database, and iterate over them.
45///
46/// ```no_run
47/// use spin_sdk::mysql::{Connection, Decode, ParameterValue};
48///
49/// # fn run() -> anyhow::Result<()> {
50/// # let min_age = 0;
51/// let db = Connection::open("mysql://root:my_password@localhost/mydb")?;
52///
53/// let query_result = db.query(
54///     "SELECT * FROM users WHERE age < ?",
55///     &[ParameterValue::Int32(20)]
56/// )?;
57///
58/// let name_index = query_result.columns.iter().position(|c| c.name == "name").unwrap();
59///
60/// for row in &query_result.rows {
61///     let name = String::decode(&row[name_index])?;
62///     println!("Found user {name}");
63/// }
64/// # Ok(())
65/// # }
66/// ```
67///
68/// Perform an aggregate (scalar) operation over a table. The result set
69/// contains a single column, with a single row.
70///
71/// ```no_run
72/// use spin_sdk::mysql::{Connection, Decode};
73///
74/// # fn run() -> anyhow::Result<()> {
75/// let db = Connection::open("mysql://root:my_password@localhost/mydb")?;
76///
77/// let query_result = db.query("SELECT COUNT(*) FROM users", &[])?;
78///
79/// assert_eq!(1, query_result.columns.len());
80/// assert_eq!("COUNT(*)", query_result.columns[0].name);
81/// assert_eq!(1, query_result.rows.len());
82///
83/// let count = i64::decode(&query_result.rows[0][0])?;
84/// # Ok(())
85/// # }
86/// ```
87///
88/// Delete rows from a MySQL table. This uses [Connection::execute()]
89/// instead of the `query` method.
90///
91/// ```no_run
92/// use spin_sdk::mysql::{Connection, ParameterValue};
93///
94/// # fn run() -> anyhow::Result<()> {
95/// let db = Connection::open("mysql://root:my_password@localhost/mydb")?;
96///
97/// let rows_affected = db.execute(
98///     "DELETE FROM users WHERE name = ?",
99///     &[ParameterValue::Str("Baldrick".to_owned())]
100/// )?;
101/// # Ok(())
102/// # }
103/// ```
104#[doc(inline)]
105pub use wit::fermyon::spin::mysql::Connection;
106
107/// The result of a database query.
108///
109/// # Examples
110///
111/// Load a set of rows from a local MySQL database, and iterate over them
112/// selecting one field from each. The columns collection allows you to find
113/// column indexes for column names; you can bypass this lookup if you name
114/// specific columns in the query.
115///
116/// ```no_run
117/// use spin_sdk::mysql::{Connection, Decode, ParameterValue};
118///
119/// # fn run() -> anyhow::Result<()> {
120/// # let min_age = 0;
121/// let db = Connection::open("mysql://root:my_password@localhost/mydb")?;
122///
123/// let query_result = db.query(
124///     "SELECT * FROM users WHERE age >= ?",
125///     &[ParameterValue::Int32(min_age)]
126/// )?;
127///
128/// let name_index = query_result.columns.iter().position(|c| c.name == "name").unwrap();
129///
130/// for row in &query_result.rows {
131///     let name = String::decode(&row[name_index])?;
132///     println!("Found user {name}");
133/// }
134/// # Ok(())
135/// # }
136/// ```
137#[doc(inline)]
138pub use wit::fermyon::spin::mysql::RowSet;
139
140#[doc(inline)]
141pub use wit::fermyon::spin::mysql::Error as MysqlError;
142
143#[doc(inline)]
144pub use wit::fermyon::spin::rdbms_types::*;
145
146/// A MySQL error
147#[derive(Debug, thiserror::Error)]
148pub enum Error {
149    /// Failed to deserialize [`DbValue`]
150    #[error("error value decoding: {0}")]
151    Decode(String),
152    /// MySQL query failed with an error
153    #[error(transparent)]
154    MysqlError(#[from] MysqlError),
155}
156
157/// A type that can be decoded from the database.
158pub trait Decode: Sized {
159    /// Decode a new value of this type using a [`DbValue`].
160    fn decode(value: &DbValue) -> Result<Self, Error>;
161}
162
163impl<T> Decode for Option<T>
164where
165    T: Decode,
166{
167    fn decode(value: &DbValue) -> Result<Self, Error> {
168        match value {
169            DbValue::DbNull => Ok(None),
170            v => Ok(Some(T::decode(v)?)),
171        }
172    }
173}
174
175impl Decode for bool {
176    fn decode(value: &DbValue) -> Result<Self, Error> {
177        match value {
178            DbValue::Int8(0) => Ok(false),
179            DbValue::Int8(1) => Ok(true),
180            _ => Err(Error::Decode(format_decode_err(
181                "TINYINT(1), BOOLEAN",
182                value,
183            ))),
184        }
185    }
186}
187
188impl Decode for i8 {
189    fn decode(value: &DbValue) -> Result<Self, Error> {
190        match value {
191            DbValue::Int8(n) => Ok(*n),
192            _ => Err(Error::Decode(format_decode_err("TINYINT", value))),
193        }
194    }
195}
196
197impl Decode for i16 {
198    fn decode(value: &DbValue) -> Result<Self, Error> {
199        match value {
200            DbValue::Int16(n) => Ok(*n),
201            _ => Err(Error::Decode(format_decode_err("SMALLINT", value))),
202        }
203    }
204}
205
206impl Decode for i32 {
207    fn decode(value: &DbValue) -> Result<Self, Error> {
208        match value {
209            DbValue::Int32(n) => Ok(*n),
210            _ => Err(Error::Decode(format_decode_err("INT", value))),
211        }
212    }
213}
214
215impl Decode for i64 {
216    fn decode(value: &DbValue) -> Result<Self, Error> {
217        match value {
218            DbValue::Int64(n) => Ok(*n),
219            _ => Err(Error::Decode(format_decode_err("BIGINT", value))),
220        }
221    }
222}
223
224impl Decode for u8 {
225    fn decode(value: &DbValue) -> Result<Self, Error> {
226        match value {
227            DbValue::Uint8(n) => Ok(*n),
228            _ => Err(Error::Decode(format_decode_err("UNSIGNED TINYINT", value))),
229        }
230    }
231}
232
233impl Decode for u16 {
234    fn decode(value: &DbValue) -> Result<Self, Error> {
235        match value {
236            DbValue::Uint16(n) => Ok(*n),
237            _ => Err(Error::Decode(format_decode_err("UNSIGNED SMALLINT", value))),
238        }
239    }
240}
241
242impl Decode for u32 {
243    fn decode(value: &DbValue) -> Result<Self, Error> {
244        match value {
245            DbValue::Uint32(n) => Ok(*n),
246            _ => Err(Error::Decode(format_decode_err(
247                "UNISIGNED MEDIUMINT, UNSIGNED INT",
248                value,
249            ))),
250        }
251    }
252}
253
254impl Decode for u64 {
255    fn decode(value: &DbValue) -> Result<Self, Error> {
256        match value {
257            DbValue::Uint64(n) => Ok(*n),
258            _ => Err(Error::Decode(format_decode_err("UNSIGNED BIGINT", value))),
259        }
260    }
261}
262
263impl Decode for f32 {
264    fn decode(value: &DbValue) -> Result<Self, Error> {
265        match value {
266            DbValue::Floating32(n) => Ok(*n),
267            _ => Err(Error::Decode(format_decode_err("FLOAT", value))),
268        }
269    }
270}
271
272impl Decode for f64 {
273    fn decode(value: &DbValue) -> Result<Self, Error> {
274        match value {
275            DbValue::Floating64(n) => Ok(*n),
276            _ => Err(Error::Decode(format_decode_err("DOUBLE", value))),
277        }
278    }
279}
280
281impl Decode for Vec<u8> {
282    fn decode(value: &DbValue) -> Result<Self, Error> {
283        match value {
284            DbValue::Binary(n) => Ok(n.to_owned()),
285            _ => Err(Error::Decode(format_decode_err("BINARY, VARBINARY", value))),
286        }
287    }
288}
289
290impl Decode for String {
291    fn decode(value: &DbValue) -> Result<Self, Error> {
292        match value {
293            DbValue::Str(s) => Ok(s.to_owned()),
294            _ => Err(Error::Decode(format_decode_err(
295                "CHAR, VARCHAR, TEXT",
296                value,
297            ))),
298        }
299    }
300}
301
302fn format_decode_err(types: &str, value: &DbValue) -> String {
303    format!("Expected {} from the DB but got {:?}", types, value)
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn boolean() {
312        assert!(bool::decode(&DbValue::Int8(1)).unwrap());
313        assert!(bool::decode(&DbValue::Int8(3)).is_err());
314        assert!(bool::decode(&DbValue::Int32(0)).is_err());
315        assert!(Option::<bool>::decode(&DbValue::DbNull).unwrap().is_none());
316    }
317
318    #[test]
319    fn int8() {
320        assert_eq!(i8::decode(&DbValue::Int8(0)).unwrap(), 0);
321        assert!(i8::decode(&DbValue::Int32(0)).is_err());
322        assert!(Option::<i8>::decode(&DbValue::DbNull).unwrap().is_none());
323    }
324
325    #[test]
326    fn int16() {
327        assert_eq!(i16::decode(&DbValue::Int16(0)).unwrap(), 0);
328        assert!(i16::decode(&DbValue::Int32(0)).is_err());
329        assert!(Option::<i16>::decode(&DbValue::DbNull).unwrap().is_none());
330    }
331
332    #[test]
333    fn int32() {
334        assert_eq!(i32::decode(&DbValue::Int32(0)).unwrap(), 0);
335        assert!(i32::decode(&DbValue::Boolean(false)).is_err());
336        assert!(Option::<i32>::decode(&DbValue::DbNull).unwrap().is_none());
337    }
338
339    #[test]
340    fn int64() {
341        assert_eq!(i64::decode(&DbValue::Int64(0)).unwrap(), 0);
342        assert!(i64::decode(&DbValue::Boolean(false)).is_err());
343        assert!(Option::<i64>::decode(&DbValue::DbNull).unwrap().is_none());
344    }
345
346    #[test]
347    fn uint8() {
348        assert_eq!(u8::decode(&DbValue::Uint8(0)).unwrap(), 0);
349        assert!(u8::decode(&DbValue::Uint32(0)).is_err());
350        assert!(Option::<u16>::decode(&DbValue::DbNull).unwrap().is_none());
351    }
352
353    #[test]
354    fn uint16() {
355        assert_eq!(u16::decode(&DbValue::Uint16(0)).unwrap(), 0);
356        assert!(u16::decode(&DbValue::Uint32(0)).is_err());
357        assert!(Option::<u16>::decode(&DbValue::DbNull).unwrap().is_none());
358    }
359
360    #[test]
361    fn uint32() {
362        assert_eq!(u32::decode(&DbValue::Uint32(0)).unwrap(), 0);
363        assert!(u32::decode(&DbValue::Boolean(false)).is_err());
364        assert!(Option::<u32>::decode(&DbValue::DbNull).unwrap().is_none());
365    }
366
367    #[test]
368    fn uint64() {
369        assert_eq!(u64::decode(&DbValue::Uint64(0)).unwrap(), 0);
370        assert!(u64::decode(&DbValue::Boolean(false)).is_err());
371        assert!(Option::<u64>::decode(&DbValue::DbNull).unwrap().is_none());
372    }
373
374    #[test]
375    fn floating32() {
376        assert!(f32::decode(&DbValue::Floating32(0.0)).is_ok());
377        assert!(f32::decode(&DbValue::Boolean(false)).is_err());
378        assert!(Option::<f32>::decode(&DbValue::DbNull).unwrap().is_none());
379    }
380
381    #[test]
382    fn floating64() {
383        assert!(f64::decode(&DbValue::Floating64(0.0)).is_ok());
384        assert!(f64::decode(&DbValue::Boolean(false)).is_err());
385        assert!(Option::<f64>::decode(&DbValue::DbNull).unwrap().is_none());
386    }
387
388    #[test]
389    fn str() {
390        assert_eq!(
391            String::decode(&DbValue::Str(String::from("foo"))).unwrap(),
392            String::from("foo")
393        );
394
395        assert!(String::decode(&DbValue::Int32(0)).is_err());
396        assert!(Option::<String>::decode(&DbValue::DbNull)
397            .unwrap()
398            .is_none());
399    }
400}