Skip to main content

keelson_exec/
row.rs

1use std::sync::Arc;
2
3use keelson_core::{FromValue, Value};
4
5use crate::error::ExecError;
6
7/// One column of a result set's header.
8#[derive(Debug, Clone)]
9pub struct Column {
10    name: String,
11}
12
13impl Column {
14    /// A column with this name. Backend-facing.
15    pub fn new(name: impl Into<String>) -> Self {
16        Column { name: name.into() }
17    }
18
19    /// The column's name, exactly as the driver reported it.
20    pub fn name(&self) -> &str {
21        &self.name
22    }
23}
24
25/// One decoded row: a shared column header and one [`Value`] per column.
26///
27/// Owned, cloneable, lifetime-free and driver-free — a backend converts its
28/// native row once, at the seam, and everything above works on this. That is
29/// what makes one [`FromRow`] impl correct on every backend (the per-engine
30/// text forms are absorbed below, by `FromValue`'s documented text
31/// acceptance), and what makes row mapping testable without a database.
32#[derive(Debug, Clone)]
33pub struct Row {
34    columns: Arc<[Column]>,
35    values: Vec<Value>,
36}
37
38impl Row {
39    /// Assemble a row. Backend-facing; the header is shared across all rows
40    /// of a result set via the `Arc`.
41    pub fn new(columns: Arc<[Column]>, values: Vec<Value>) -> Self {
42        debug_assert_eq!(columns.len(), values.len());
43        Row { columns, values }
44    }
45
46    /// The result set's columns, in order.
47    pub fn columns(&self) -> &[Column] {
48        &self.columns
49    }
50
51    /// The raw value under `name`, if such a column exists.
52    ///
53    /// Duplicate column names resolve to the first, documented and matching
54    /// sqlx; reach the rest by position.
55    pub fn value(&self, name: &str) -> Option<&Value> {
56        self.index_of(name).map(|i| &self.values[i])
57    }
58
59    /// Read the column `name` as `T`. Clones the value; derived mappers use
60    /// [`take`](Self::take) instead so `String`/`Vec<u8>`/JSON move.
61    pub fn get<T: FromValue>(&self, name: &str) -> Result<T, ExecError> {
62        let i = self.require(name)?;
63        decode(&self.columns[i].name, self.values[i].clone())
64    }
65
66    /// Read the column at `index` as `T`.
67    pub fn get_at<T: FromValue>(&self, index: usize) -> Result<T, ExecError> {
68        let v = self.value_at(index)?.clone();
69        decode(&positional_label(index, &self.columns), v)
70    }
71
72    /// Take the column `name` out of the row as `T`, leaving `NULL` behind.
73    ///
74    /// The consuming variant derived/generated `FromRow` impls use: `String`,
75    /// `Vec<u8>` and JSON documents move rather than clone.
76    pub fn take<T: FromValue>(&mut self, name: &str) -> Result<T, ExecError> {
77        let i = self.require(name)?;
78        let v = std::mem::replace(&mut self.values[i], Value::Null);
79        decode(&self.columns[i].name, v)
80    }
81
82    /// Take the column at `index` out of the row as `T`.
83    pub fn take_at<T: FromValue>(&mut self, index: usize) -> Result<T, ExecError> {
84        if index >= self.values.len() {
85            return Err(missing(&format!("#{index}"), &self.columns));
86        }
87        let v = std::mem::replace(&mut self.values[index], Value::Null);
88        decode(&positional_label(index, &self.columns), v)
89    }
90
91    fn index_of(&self, name: &str) -> Option<usize> {
92        self.columns.iter().position(|c| c.name == name)
93    }
94
95    fn require(&self, name: &str) -> Result<usize, ExecError> {
96        self.index_of(name)
97            .ok_or_else(|| missing(name, &self.columns))
98    }
99
100    fn value_at(&self, index: usize) -> Result<&Value, ExecError> {
101        self.values
102            .get(index)
103            .ok_or_else(|| missing(&format!("#{index}"), &self.columns))
104    }
105}
106
107/// The error label for positional access: the real name when the header has
108/// one, `#N` otherwise.
109fn positional_label(index: usize, columns: &[Column]) -> String {
110    match columns.get(index) {
111        Some(c) if !c.name.is_empty() => c.name.clone(),
112        _ => format!("#{index}"),
113    }
114}
115
116fn missing(column: &str, columns: &[Column]) -> ExecError {
117    ExecError::MissingColumn {
118        column: column.to_owned(),
119        available: columns.iter().map(|c| c.name.clone()).collect(),
120    }
121}
122
123/// Weave the column name into a `FromValue` failure — this is the boundary
124/// where "cannot read NULL as String" becomes "column \"email\": cannot read
125/// NULL as String".
126fn decode<T: FromValue>(column: &str, v: Value) -> Result<T, ExecError> {
127    T::from_value(v).map_err(|source| ExecError::Decode {
128        column: column.to_owned(),
129        source,
130    })
131}
132
133/// A type that can be built from a whole row.
134///
135/// By name for structs (survives column reordering and `SELECT *` drift; the
136/// mapping generated models will use), by position for tuples (quick ad-hoc
137/// reads). The hand-written shape — the same one code generation will emit —
138/// is one [`take`](Row::take) per field:
139///
140/// ```
141/// use keelson_exec::{ExecError, FromRow, Row};
142///
143/// struct User {
144///     id: i64,
145///     email: Option<String>, // a nullable column must be an Option
146/// }
147///
148/// impl FromRow for User {
149///     fn from_row(row: &mut Row) -> Result<Self, ExecError> {
150///         Ok(User {
151///             id: row.take("id")?,
152///             email: row.take("email")?,
153///         })
154///     }
155/// }
156/// ```
157pub trait FromRow: Sized {
158    /// Build `Self` out of `row`. Takes `&mut` so values move, never clone.
159    fn from_row(row: &mut Row) -> Result<Self, ExecError>;
160}
161
162/// A row reads as itself, so `fetch_all::<Row>` hands back raw rows.
163impl FromRow for Row {
164    fn from_row(row: &mut Row) -> Result<Self, ExecError> {
165        Ok(row.clone())
166    }
167}
168
169macro_rules! from_row_tuple {
170    ($($idx:tt : $t:ident),+) => {
171        /// Positional: element `N` reads column `N`.
172        impl<$($t: FromValue),+> FromRow for ($($t,)+) {
173            fn from_row(row: &mut Row) -> Result<Self, ExecError> {
174                Ok(($(row.take_at::<$t>($idx)?,)+))
175            }
176        }
177    };
178}
179
180from_row_tuple!(0: A);
181from_row_tuple!(0: A, 1: B);
182from_row_tuple!(0: A, 1: B, 2: C);
183from_row_tuple!(0: A, 1: B, 2: C, 3: D);
184from_row_tuple!(0: A, 1: B, 2: C, 3: D, 4: E);
185from_row_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F);
186from_row_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G);
187from_row_tuple!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H);
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    fn row() -> Row {
194        let columns: Arc<[Column]> =
195            vec![Column::new("id"), Column::new("name"), Column::new("email")].into();
196        Row::new(
197            columns,
198            vec![Value::I64(7), Value::Text("ada".into()), Value::Null],
199        )
200    }
201
202    #[test]
203    fn get_by_name_and_position() {
204        let r = row();
205        assert_eq!(r.get::<i64>("id").unwrap(), 7);
206        assert_eq!(r.get_at::<String>(1).unwrap(), "ada");
207        assert_eq!(r.get::<Option<String>>("email").unwrap(), None);
208        assert_eq!(r.value("id"), Some(&Value::I64(7)));
209        assert_eq!(r.value("nope"), None);
210    }
211
212    #[test]
213    fn take_moves_the_value_out() {
214        let mut r = row();
215        assert_eq!(r.take::<String>("name").unwrap(), "ada");
216        // Taken: what is left behind is NULL.
217        assert_eq!(r.value("name"), Some(&Value::Null));
218    }
219
220    #[test]
221    fn null_into_non_option_names_the_column() {
222        let r = row();
223        let e = r.get::<String>("email").unwrap_err();
224        assert_eq!(
225            e.to_string(),
226            "column \"email\": cannot read NULL as String"
227        );
228    }
229
230    #[test]
231    fn type_mismatch_names_the_column_even_positionally() {
232        let r = row();
233        let e = r.get_at::<i64>(1).unwrap_err();
234        assert_eq!(e.to_string(), "column \"name\": cannot read text as i64");
235    }
236
237    #[test]
238    fn missing_column_lists_the_available_ones() {
239        let r = row();
240        let e = r.get::<i64>("emial").unwrap_err();
241        assert_eq!(
242            e.to_string(),
243            "no column \"emial\" in result set (columns: id, name, email)"
244        );
245        let e = r.get_at::<i64>(9).unwrap_err();
246        assert!(e.to_string().contains("#9"));
247    }
248
249    #[test]
250    fn tuples_read_positionally() {
251        let mut r = row();
252        let (id, name, email) = <(i64, String, Option<String>)>::from_row(&mut r).unwrap();
253        assert_eq!((id, name.as_str(), email), (7, "ada", None));
254    }
255
256    #[test]
257    fn a_struct_maps_by_name_with_the_documented_pattern() {
258        struct User {
259            id: i64,
260            name: String,
261            email: Option<String>,
262        }
263        impl FromRow for User {
264            fn from_row(row: &mut Row) -> Result<Self, ExecError> {
265                Ok(User {
266                    id: row.take("id")?,
267                    name: row.take("name")?,
268                    email: row.take("email")?,
269                })
270            }
271        }
272        let u = User::from_row(&mut row()).unwrap();
273        assert_eq!((u.id, u.name.as_str(), u.email), (7, "ada", None));
274    }
275}