Skip to main content

rustlavel_db/
row.rs

1//! A result row.
2
3use crate::value::{FromValue, Value};
4use rustlavel_core::{Error, Json, Result};
5use std::sync::Arc;
6
7/// The column names of a result set, shared by every row in it.
8pub type Columns = Arc<Vec<String>>;
9
10#[derive(Debug, Clone)]
11pub struct Row {
12    columns: Columns,
13    values: Vec<Value>,
14}
15
16impl Row {
17    pub fn new(columns: Columns, values: Vec<Value>) -> Self {
18        Row { columns, values }
19    }
20
21    pub fn columns(&self) -> &[String] {
22        &self.columns
23    }
24
25    pub fn len(&self) -> usize {
26        self.values.len()
27    }
28
29    pub fn is_empty(&self) -> bool {
30        self.values.is_empty()
31    }
32
33    /// Read a column by name, converted to `T`.
34    ///
35    /// The error names the column, because "invalid type" with no column name
36    /// is the least useful message a database layer can produce.
37    pub fn get<T: FromValue>(&self, column: &str) -> Result<T> {
38        let value = self.value(column)?;
39        T::from_value(value).map_err(|e| Error::msg(format!("column `{column}`: {e}")))
40    }
41
42    /// Read a column by position.
43    pub fn get_at<T: FromValue>(&self, index: usize) -> Result<T> {
44        let value = self
45            .values
46            .get(index)
47            .ok_or_else(|| Error::msg(format!("no column at index {index}")))?;
48        T::from_value(value)
49    }
50
51    /// Read a column, falling back when it is absent or NULL.
52    pub fn get_or<T: FromValue>(&self, column: &str, default: T) -> T {
53        self.value(column).ok().and_then(|v| T::from_value(v).ok()).unwrap_or(default)
54    }
55
56    pub fn value(&self, column: &str) -> Result<&Value> {
57        let index = self
58            .columns
59            .iter()
60            .position(|name| name == column)
61            .ok_or_else(|| self.unknown_column(column))?;
62        Ok(&self.values[index])
63    }
64
65    /// The value at a position, for a result whose columns the database did
66    /// not name.
67    pub fn value_at(&self, index: usize) -> Result<&Value> {
68        self.values
69            .get(index)
70            .ok_or_else(|| Error::msg(format!("no column at index {index}")))
71    }
72
73    pub fn has(&self, column: &str) -> bool {
74        self.columns.iter().any(|name| name == column)
75    }
76
77    fn unknown_column(&self, column: &str) -> Error {
78        Error::msg(format!(
79            "no column `{column}` in this result. Available: {}",
80            self.columns.join(", ")
81        ))
82    }
83
84    /// The row as a JSON object — what an API handler usually wants next.
85    pub fn to_json(&self) -> Json {
86        Json::object(
87            self.columns
88                .iter()
89                .cloned()
90                .zip(self.values.iter().cloned().map(Json::from))
91                .collect::<Vec<_>>(),
92        )
93    }
94}
95
96/// Turn a whole result set into a JSON array.
97pub fn rows_to_json(rows: &[Row]) -> Json {
98    Json::Array(rows.iter().map(Row::to_json).collect())
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    fn row() -> Row {
106        let columns = Arc::new(vec!["id".to_string(), "name".to_string(), "note".to_string()]);
107        Row::new(columns, vec![Value::Int(7), Value::Text("Ada".into()), Value::Null])
108    }
109
110    #[test]
111    fn reads_columns_by_name_and_position() {
112        let row = row();
113
114        assert_eq!(row.get::<i64>("id").unwrap(), 7);
115        assert_eq!(row.get::<String>("name").unwrap(), "Ada");
116        assert_eq!(row.get::<Option<String>>("note").unwrap(), None);
117        assert_eq!(row.get_at::<i64>(0).unwrap(), 7);
118    }
119
120    #[test]
121    fn an_unknown_column_lists_what_is_available() {
122        let error = row().get::<i64>("nope").unwrap_err().to_string();
123
124        assert!(error.contains("no column `nope`"));
125        assert!(error.contains("id, name, note"));
126    }
127
128    #[test]
129    fn a_type_error_names_the_column() {
130        let error = row().get::<i64>("name").unwrap_err().to_string();
131        assert!(error.contains("column `name`"));
132    }
133
134    #[test]
135    fn converts_to_json() {
136        assert_eq!(
137            row().to_json().to_string(),
138            r#"{"id":7,"name":"Ada","note":null}"#
139        );
140    }
141}