Skip to main content

siderite_orm/
decode.rs

1//! Typed decoding of dynamic result rows: [`FromValues`] for `values_list`,
2//! [`Row::get_as`] and [`QueryResult::decode`] for raw SQL.
3
4use crate::backend::{QueryResult, Row};
5use crate::error::QueryError;
6use crate::model::Model;
7use crate::types::{DbType, decode};
8use crate::value::Value;
9
10/// A type decoded from the columns of a row by position: a single
11/// [`DbType`] for one column, or a tuple of up to six.
12///
13/// ```ignore
14/// let rows: Vec<(String, i64)> = Book::objects(&db)
15///     .values_list([Book::title.select(), Book::likes.select()])
16///     .await?;
17/// ```
18pub trait FromValues: Sized {
19    /// Decode `row`.
20    ///
21    /// # Errors
22    /// [`QueryError::Decode`] if a column is missing or has the wrong type.
23    fn from_values(row: &Row) -> Result<Self, QueryError>;
24}
25
26impl<T: DbType> FromValues for T {
27    fn from_values(row: &Row) -> Result<Self, QueryError> {
28        row.decode_at(0)
29    }
30}
31
32macro_rules! tuple_from_values {
33    ($($name:ident : $index:tt),+) => {
34        impl<$($name: DbType),+> FromValues for ($($name,)+) {
35            fn from_values(row: &Row) -> Result<Self, QueryError> {
36                Ok(($(row.decode_at::<$name>($index)?,)+))
37            }
38        }
39    };
40}
41tuple_from_values!(A: 0);
42tuple_from_values!(A: 0, B: 1);
43tuple_from_values!(A: 0, B: 1, C: 2);
44tuple_from_values!(A: 0, B: 1, C: 2, D: 3);
45tuple_from_values!(A: 0, B: 1, C: 2, D: 3, E: 4);
46tuple_from_values!(A: 0, B: 1, C: 2, D: 3, E: 4, F: 5);
47
48impl Row {
49    /// Value of `column`, decoded as `T`.
50    ///
51    /// # Errors
52    /// [`QueryError::Decode`] if the column is missing or has the wrong type.
53    pub fn get_as<T: DbType>(&self, column: &str) -> Result<T, QueryError> {
54        let value = self
55            .get(column)
56            .cloned()
57            .ok_or_else(|| QueryError::Decode {
58                column: column.to_owned(),
59                reason: "column missing from result".into(),
60            })?;
61        decode(column, value)
62    }
63
64    /// The `index`-th column, decoded as `T`.
65    ///
66    /// # Errors
67    /// [`QueryError::Decode`] if there is no such column or it has the wrong type.
68    pub fn decode_at<T: DbType>(&self, index: usize) -> Result<T, QueryError> {
69        let (name, value) = self.iter().nth(index).ok_or_else(|| QueryError::Decode {
70            column: format!("#{index}"),
71            reason: format!("row has only {} columns", self.len()),
72        })?;
73        decode(name, value.clone())
74    }
75}
76
77impl QueryResult {
78    /// Decode every row as model `M` (columns looked up by name).
79    ///
80    /// # Errors
81    /// [`QueryError::Decode`] for the first row that does not fit.
82    pub fn decode<M: Model>(&self) -> Result<Vec<M>, QueryError> {
83        self.rows.iter().map(|row| M::from_row(row, "")).collect()
84    }
85
86    /// Decode every row by column position as `T` (a scalar or a tuple).
87    ///
88    /// # Errors
89    /// [`QueryError::Decode`] for the first row that does not fit.
90    pub fn decode_values<T: FromValues>(&self) -> Result<Vec<T>, QueryError> {
91        self.rows.iter().map(T::from_values).collect()
92    }
93
94    /// The first column of the first row, if any.
95    pub fn scalar(&self) -> Option<&Value> {
96        self.rows.first()?.iter().next().map(|(_, v)| v)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn row() -> Row {
105        Row::new(vec![
106            ("name".into(), Value::Text("ann".into())),
107            ("age".into(), Value::Int(30)),
108            ("nick".into(), Value::Null),
109        ])
110    }
111
112    #[test]
113    fn tuples_decode_by_position() {
114        let (name, age, nick) =
115            <(String, i64, Option<String>)>::from_values(&row()).unwrap_or_default();
116        assert_eq!((name.as_str(), age, nick), ("ann", 30, None));
117        assert_eq!(String::from_values(&row()), Ok("ann".to_owned()));
118    }
119
120    #[test]
121    fn errors_name_the_offending_column() {
122        let err = <(i64,)>::from_values(&row());
123        assert!(matches!(err, Err(QueryError::Decode { column, .. }) if column == "name"));
124        let short = <(String, i64, Option<String>, i64)>::from_values(&row());
125        assert!(matches!(short, Err(QueryError::Decode { column, .. }) if column == "#3"));
126    }
127
128    #[test]
129    fn rows_decode_by_name_and_decimals_fit_integers() {
130        assert_eq!(row().get_as::<i64>("age"), Ok(30));
131        assert!(row().get_as::<i64>("missing").is_err());
132        let sum = Row::new(vec![(
133            "s".into(),
134            Value::Decimal(rust_decimal::Decimal::new(27, 0)),
135        )]);
136        assert_eq!(sum.get_as::<i64>("s"), Ok(27));
137        assert_eq!(sum.get_as::<f64>("s"), Ok(27.0));
138        let fractional = Row::new(vec![(
139            "s".into(),
140            Value::Decimal(rust_decimal::Decimal::new(275, 1)),
141        )]);
142        assert!(fractional.get_as::<i64>("s").is_err());
143    }
144}